{"id":50956412,"url":"https://github.com/fifoqueue/shortlink","last_synced_at":"2026-06-18T07:30:54.016Z","repository":{"id":363633574,"uuid":"1263224931","full_name":"fifoqueue/shortlink","owner":"fifoqueue","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-18T01:41:12.000Z","size":2388,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-18T03:14:53.346Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fifoqueue.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-08T18:41:17.000Z","updated_at":"2026-06-18T01:41:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/fifoqueue/shortlink","commit_stats":null,"previous_names":["fifoqueue/shortlink"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/fifoqueue/shortlink","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fifoqueue%2Fshortlink","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fifoqueue%2Fshortlink/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fifoqueue%2Fshortlink/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fifoqueue%2Fshortlink/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fifoqueue","download_url":"https://codeload.github.com/fifoqueue/shortlink/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fifoqueue%2Fshortlink/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34481187,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-18T07:30:53.208Z","updated_at":"2026-06-18T07:30:54.011Z","avatar_url":"https://github.com/fifoqueue.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Shortlink\n\nSvelte 5와 SvelteKit으로 만든 자체 호스팅 단축 링크 서비스입니다. 링크, 사용자, 설정, 클릭 통계는 PostgreSQL에 저장하고, 서버는 SvelteKit adapter-node 결과물로 실행합니다.\n\n## 요구 사항\n\n- Node.js 24.0.0 이상\n- Yarn 4.16.0. 이 레포지토리는 `packageManager`와 `.yarn/releases`를 포함합니다.\n- PostgreSQL. 부분 검색 성능을 위해 `pg_trgm` 확장 설치를 권장합니다.\n\n처음 클론한 환경에서는 Corepack을 켜두는 것이 가장 단순합니다.\n\n```bash\ncorepack enable\n```\n\n## 빠른 시작\n\n1. 레포지토리를 클론하고 의존성을 설치합니다.\n\n```bash\ngit clone \u003crepo-url\u003e\ncd shortlink-sveltekit\nyarn install\n```\n\n2. PostgreSQL에 데이터베이스와 사용자를 만듭니다.\n\n```sql\nCREATE USER shortlink WITH PASSWORD 'replace-this-password';\nCREATE DATABASE shortlink OWNER shortlink;\n```\n\n3. 환경 파일을 만들고 값을 수정합니다.\n\n```bash\ncp .env.example .env\n```\n\n최소한 `DATABASE_URL`과 `AUTH_SESSION_SECRET`은 실제 값으로 바꿔야 합니다.\n\n```dotenv\nDATABASE_URL=postgres://shortlink:password@localhost:5432/shortlink\nDATABASE_SSL=false\nDATABASE_LOGGING=false\nPRIVATE_BASE_URL=\nHOST=127.0.0.1\nAUTH_SESSION_SECRET=replace-with-a-long-random-secret\n```\n\n4. 개발 서버를 실행합니다.\n\n```bash\nyarn dev\n```\n\n브라우저에서 `http://localhost:5173`을 엽니다. 아직 사용자가 없으면 `/admin` 접근 시 `/signup`으로 이동하며, 첫 번째 가입 계정이 관리자가 됩니다.\n\n개발 서버 포트나 바인딩 주소는 Vite 옵션으로 지정합니다.\n\n```bash\nyarn dev -- --host 0.0.0.0 --port 3000\n```\n\n`.env`의 `HOST`와 `PORT`는 빌드된 Node 서버용입니다. Vite 개발 서버 주소를 바꾸려면 위처럼 CLI 옵션을 사용하세요.\n\n## 환경 변수\n\n| 이름                  | 필수   | 설명                                                                                                                                     |\n| --------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------- |\n| `DATABASE_URL`        | 예     | PostgreSQL 연결 문자열입니다.                                                                                                            |\n| `AUTH_SESSION_SECRET` | 예     | 로그인 세션 쿠키 서명에 사용합니다. 충분히 긴 임의 문자열을 사용하세요.                                                                  |\n| `DATABASE_SSL`        | 아니오 | `true`이면 PostgreSQL TLS 연결을 사용합니다.                                                                                             |\n| `DATABASE_LOGGING`    | 아니오 | `true`이면 Sequelize SQL 로그를 출력합니다.                                                                                              |\n| `PRIVATE_BASE_URL`    | 아니오 | 생성되는 단축 URL의 공개 base URL입니다. 리버스 프록시 뒤 운영 시 실제 공개 주소를 넣으세요.                                             |\n| `HOST`                | 아니오 | 빌드된 adapter-node 서버의 bind host입니다. 기본 예시는 `127.0.0.1`입니다.                                                               |\n| `PORT`                | 아니오 | 빌드된 adapter-node 서버 포트입니다. 설정하지 않으면 adapter 기본값을 사용합니다.                                                        |\n| `ORIGIN`              | 아니오 | SvelteKit adapter-node가 요청 origin을 판단할 때 쓰는 공개 origin입니다. 운영에서는 `https://go.example.com`처럼 실제 주소를 권장합니다. |\n\n## 관리자 패널\n\n관리자는 `/admin`에서 다음 영역을 설정합니다.\n\n- 사이트: 공개 범위, 브랜드 문구, SEO, 회원 가입, 이메일 인증, 비밀번호 정책\n- 링크 및 API: 커스텀 코드 길이, 자동 코드 길이, 클릭 추적, 리다이렉트 상태, URL 해시 제거, 허용 scheme, 차단 도메인, API 기능, 리버스 프록시 IP 헤더\n- 테마: 프리셋, 라이트/다크/시스템 모드, 색상 토큰, radius, 글꼴, 커스텀 CSS\n- 플러그인: CAPTCHA, 속도 제한, OIDC SSO, 사용자 관리, 향상된 추적 등 플러그인 설정\n- 링크 관리: 저장된 링크 검색, 수정, 삭제, 통계 확인\n\n이 레포지토리가 제공하는 코어 플러그인은 `src/plugins/\u003cplugin-id\u003e/`에 두고, 서버별 유저 플러그인은 `src/user-plugins/\u003cplugin-id\u003e/`에 둡니다. 유저 플러그인은 기본적으로 git 추적에서 제외됩니다. 빌드 시점 플러그인 개발 계약은 [docs/plugin-development.md](docs/plugin-development.md)를, Docker 이미지 재빌드 없이 로드할 런타임 플러그인 ABI 설계는 [docs/runtime-plugin-abi.md](docs/runtime-plugin-abi.md)를 참고하세요.\n\n## 리버스 프록시와 실제 IP\n\n운영에서 앱을 Nginx, Caddy, Cloudflare 같은 프록시 뒤에 둘 때는 `HOST=127.0.0.1`로 Node 서버를 로컬에만 열고, 프록시가 외부 HTTPS를 받도록 구성하는 방식이 일반적입니다.\n\nNginx 예시:\n\n```nginx\nlocation / {\n  proxy_pass http://127.0.0.1:3000;\n  proxy_http_version 1.1;\n  proxy_set_header Host $host;\n  proxy_set_header X-Forwarded-Proto $scheme;\n  proxy_set_header X-Forwarded-For $remote_addr;\n  proxy_set_header X-Real-IP $remote_addr;\n}\n```\n\n통계, CAPTCHA, 비로그인 링크 소유자 판별에 실제 클라이언트 IP를 사용하려면 관리자 패널의 `링크 및 API`에서 `리버스 프록시 IP 헤더 신뢰`를 켜고 `신뢰할 IP 헤더` 목록을 확인하세요.\n\n기본 헤더 순서는 다음과 같습니다.\n\n```text\nForwarded\nX-Forwarded-For\nX-Real-IP\nCF-Connecting-IP\nTrue-Client-IP\n```\n\n이 설정을 켜면 앱은 저장된 순서대로 헤더를 읽고, 유효한 IP가 없으면 직접 연결 주소로 되돌아갑니다. 앱이 공개 인터넷에서 직접 접근 가능한 구성에서는 클라이언트가 IP 헤더를 위조할 수 있으므로 켜지 마세요.\n\n## 프로덕션 실행\n\n빌드합니다.\n\n```bash\nyarn build\n```\n\nNode로 직접 실행할 수 있습니다.\n\n```bash\nORIGIN=https://go.example.com node --env-file=.env build\n```\n\n`package.json`의 `start` 스크립트를 사용할 수도 있습니다. 이 경우 `.env`를 읽도록 별도 실행 래퍼나 process manager 설정이 필요합니다.\n\n```bash\nyarn start\n```\n\n이 레포지토리에는 PM2용 [ecosystem.config.cjs](ecosystem.config.cjs)가 포함되어 있습니다. 이 설정은 `build/index.js`를 실행하고, Node의 `--env-file=.env` 옵션으로 `.env`를 읽습니다.\nPM2 설정에는 재시작 시 오래 대기하지 않도록 `SHUTDOWN_TIMEOUT=1`과 짧은 `kill_timeout`이 포함되어 있습니다.\n\n```bash\nyarn build\npm2 start ecosystem.config.cjs\n```\n\n코드를 갱신한 뒤에는 다시 빌드하고 PM2 프로세스를 재시작하거나 reload 하세요.\n\n```bash\nyarn build\npm2 restart ecosystem.config.cjs --update-env\n```\n\n## Docker 실행\n\n이 레포지토리의 [Dockerfile](Dockerfile)은 SvelteKit adapter-node 산출물을 빌드한 뒤 production 의존성만 포함하는 Alpine 기반 런타임 이미지를 만듭니다. 컨테이너 프로세스는 root가 아닌 UID/GID `10001:10001`로 실행됩니다.\n\n빌드 시점 유저 플러그인을 Docker에서 사용하려면 플러그인 소스를 `src/user-plugins/\u003cplugin-id\u003e/`에 둔 상태로 이미지를 빌드해야 합니다. Svelte 컴포넌트와 플러그인 모듈은 빌드 시점에 번들링되므로, 이미 빌드된 이미지에 런타임 volume으로 `src/user-plugins`만 마운트해도 새 플러그인은 적용되지 않습니다.\n\nDocker 이미지 재빌드 없이 플러그인을 추가하려면 런타임 ABI 형식으로 빌드한 플러그인을 `/app/user-plugins/\u003cplugin-id\u003e/`에 마운트합니다. 이 경우 `manifest.json`과 `server.mjs`가 필요하고, `USER_PLUGIN_WATCH=true`이면 파일 변경을 주기적으로 감지합니다.\n\n이미지를 직접 빌드하려면 다음처럼 실행합니다.\n\n```bash\ndocker build -t shortlink .\n```\n\n유저 플러그인을 갱신한 뒤에는 이미지를 다시 빌드하고 컨테이너를 재생성하세요.\n\n단독 컨테이너로 실행할 때는 PostgreSQL을 별도로 준비하고 환경 변수를 전달합니다.\n\n```bash\ndocker run --rm -p 3000:3000 \\\n  -e DATABASE_URL=postgres://shortlink:password@host.docker.internal:5432/shortlink \\\n  -e AUTH_SESSION_SECRET=replace-with-a-long-random-secret \\\n  -e PRIVATE_BASE_URL=https://go.example.com \\\n  shortlink\n```\n\nGitHub Actions workflow는 `main` 또는 `master`에 push될 때마다 `linux/amd64`, `linux/arm64` 이미지를 GHCR에 게시합니다.\n\n```bash\ndocker pull ghcr.io/fifoqueue/shortlink:latest\n```\n\nPostgreSQL까지 포함한 예시는 [docs/docker-compose.yaml](docs/docker-compose.yaml)을 참고하세요. 처음 실행하기 전에 `AUTH_SESSION_SECRET`, `POSTGRES_PASSWORD`, `PRIVATE_BASE_URL`은 운영 환경에 맞게 바꾸세요.\n샘플 compose 파일은 기본적으로 GHCR 이미지와 `/app/user-plugins` 런타임 플러그인 volume을 사용합니다. 빌드 시점 유저 플러그인을 포함한 커스텀 이미지를 쓰려면 compose 파일의 `build` 예시 주석을 참고해 로컬에서 빌드하세요.\n\n## 데이터베이스\n\n앱은 Sequelize 모델 기준으로 다음 테이블을 사용합니다.\n\n- `app_settings`: 사이트 설정과 플러그인 설정\n- `users`: 로컬 사용자와 관리자 여부\n- `user_identities`: 인증 플러그인별 사용자 연결\n- `api_tokens`: 사용자 API 토큰\n- `short_links`: 단축 코드, 목적지, 소유자, 공유 메타데이터\n- `click_events`: 클릭 시각, IP, user-agent, referer, 플러그인 메타데이터\n\n앱 시작 후 첫 DB 접근 시 마이그레이션과 `sequelize.sync({ alter: true })`가 한 번 실행됩니다.\n`pg_trgm` 확장이 설치되어 있으면 링크와 클릭 통계 부분 검색용 GIN trigram 인덱스를 자동 생성하려고 시도합니다. 확장이 없거나 권한이 부족하면 해당 인덱스 생성만 조용히 건너뜁니다.\n\n## API\n\nJSON API 사용법과 현재 엔드포인트 스펙은 [docs/api.md](docs/api.md)를 참고하세요.\n\n## 확인 명령\n\n코드 변경 후에는 아래 순서로 확인합니다.\n\n```bash\nyarn lint\nyarn build\n```\n\n`yarn lint`는 ESLint 자동 수정과 Prettier 포맷을 함께 실행하므로 작업 트리에 추가 변경이 생길 수 있습니다.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffifoqueue%2Fshortlink","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffifoqueue%2Fshortlink","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffifoqueue%2Fshortlink/lists"}