- Node.js: Cloudflare Workers Builds에서 지원하는
24.22.1(.nvmrc). - 패키지 매니저: 루트
package.json의pnpm@12.16.1을 Corepack으로 실행합니다. 다른 락파일은 만들지 않습니다. - 앱은
apps/web,apps/api,apps/error-automation,apps/b-bot에 둡니다. 실제 공용 코드만packages/로 분리합니다. .claude,.vscode는 개인 작업 환경이며 커밋하지 않습니다.
corepack pnpm install --frozen-lockfile
pnpm --filter @koin/api db:migrate:local
pnpm dev환경 변수·배포·데이터 이관 조건은 README를 따릅니다.
- 웹은 기존 절대 경로, API는
@api/*를 사용합니다. - 내부 화면 이동은 TanStack Router와
ROUTES를 사용합니다. - 웹 mutation 오류는
interfaces/APIError의isKoinError()로 좁힌 뒤showToast()로 표시합니다. 업무 오류가 아닌 HTTP·네트워크 오류는 같은 모듈의HttpError를 사용하며, 상태 코드는utils/ts/httpError의getErrorStatus()로 읽습니다. - 페이지 로드 스냅샷의 기기·인증·시각은 SSR에서 전달합니다. 하이드레이션 이후
utils/ssr/pageLoader는 기기는 SSR 값, 인증은 CSRF 쿠키에 결부된 세션 쿼리, 시각은 서버 오프셋을 사용합니다. 오프셋은 세션 응답의 Date로 갱신하며, 공유 캐시 문서에서 표본을 얻지 못하면 해당 이동에만 SSR 시각 기준 오프셋을 사용하고 다음 이동에서 다시 시도합니다. 실시간 시계 등 다른 클라이언트 시각에는 이 계약이 적용되지 않습니다. - 페이지 loader는
definePageLoad()로 정의해 서버 함수(loadPageData)와createPageLoader()에 같이 넘깁니다. load는 SSR과 브라우저 이동 모두에서 실행되므로 서버 전용 API를 쓰지 않고, 쿠키는context.getCookie, 쿼리는context.queryClient, 시각은serverRequest.now를 사용합니다. - 요청별 QueryClient·쿠키 전달 컨텍스트를 공유 전역 상태로 바꾸지 않습니다.
- 웹 HTTP 요청은
APIClient의 nativefetch경로를 사용합니다. 브라우저는 same-origin/api, SSR은 검증된 Worker 대상과 요청별 Cookie/Origin을 사용합니다. 타임아웃은 응답 본문 수신까지 적용하며, 브라우저 401은 세션을 한 번 갱신한 뒤 최신 CSRF 헤더로 한 번만 재시도합니다. 직접 파일 업로드는 원본 Blob을 보내고 서비스 쿠키·CSRF를 제외합니다. - D1 변경은 Drizzle의 파라미터화 SQL·원자 배치를 사용합니다. DO 직렬화만으로 DB 원자성이 생기는 것은 아닙니다.
- Queue 소비자는 중복 전달·중단·재시도를 고려하고, D1의 작업 상태와 fencing을 지킵니다.
- 주석은 변경 이력이 아니라 코드의 계약·제약을 한국어로 설명합니다.
- 오류 자동화의 번들 스택은 발생한 Worker 버전의 코드·소스맵을 대조한 뒤 해당 앱의
src/위치로 복원합니다. 과거 Tail이 없으면 인증된 원본 발생의 이벤트 ID·Ray ID·Trace ID·Worker 버전이 telemetry와 모두 일치할 때만 복원합니다. 최신 버전의 맵으로 대체하거나 다른 앱·호스트의 근거를 섞지 않습니다. 원본 코드·소스맵 내용은 진단이나 모델 입력에 저장하지 않습니다. - SCSS는 전역 reset과 초기값을 불필요하게 반복하지 않습니다.
- 별도 reviewer 봇·외부 분석 게이트를 추가하지 않습니다.
pnpm format
pnpm typecheck
pnpm lint
pnpm format:check
pnpm test
pnpm buildUI 변경은 실제 화면, API 변경은 실제 HTTP/D1 경로도 확인합니다. 실행하지 않은 검사를 통과했다고 표시하지 않습니다.
의존성 변경 시 해당 package.json, pnpm-workspace.yaml의 호환 버전·허용된 설치 스크립트, 루트 pnpm-lock.yaml을 함께 반영합니다. @types/node는 Node 24에, TypeScript·ESLint·h3는 각 도구/프레임워크의 지원 범위에 맞춥니다. pnpm up --latest 후에도 peer 호환성과 고정 버전의 근거를 확인하고, SheetJS 외부 tarball의 lockfile 무결성을 유지합니다. 생성된 route tree와 schema는 생성 원본을 변경한 뒤 갱신합니다. 적용된 D1 마이그레이션을 다시 쓰지 않습니다.
웹의 @vitejs/plugin-react@6.1.2는 React Compiler peer로 oxc-transform-react@^0.152.0을 요구합니다. 0.x의 caret 범위는 다음 minor를 포함하지 않으므로, OXC를 0.153.0 이상으로 올릴 때는 해당 버전을 지원하는 플러그인과 함께 갱신합니다.
커밋은 feat:, fix:, refactor:, chore: 등 Conventional Commit 접두사와 한국어 설명을 사용합니다. 관련 없는 변경이나 시크릿을 포함하지 않습니다. PR에 수행한 검증과 남은 운영 전제조건을 명시합니다.
커밋 훅은 Corepack으로 고정된 pnpm을 사용합니다. Windows 명령 길이 제한을 넘지 않도록 파일 목록을 분할하고, 검사 묶음은 동시에 두 개까지 실행합니다.
웹·API·오류 자동화의 production branch는 모두 main입니다. 기능·수정 PR과 자동 수정 PR은 develop을 대상으로 하며, 웹은 develop 병합 후 stage.koreatech.in에 자동 배포합니다. stage 검증 후 develop → main PR을 병합하면 Workers Builds가 검사 후 운영 배포합니다. 저장소 기본 브랜치는 develop으로 유지합니다. 운영 브랜치 변수는 KOIN_PRODUCTION_BRANCH(기본 main), staging 브랜치는 KOIN_PREVIEW_BRANCH(기본 develop)입니다. 웹·API는 develop만 고정 native Preview에 배포하고, 나머지 비운영 브랜치와 오류 자동화는 검사만 실행합니다. 웹 Preview 이름은 stage, API Preview 이름은 preview를 유지하며 별도 preview Git 브랜치는 사용하지 않습니다. API·웹·오류 자동화에 별도 stage/test Worker나 중복 GitHub Actions 배포를 만들지 않습니다. Preview 명령은 wrangler preview이고 wrangler versions upload나 --env preview가 아닙니다. Git 브랜치와 native Preview는 별개이므로 Preview·저장소 데이터를 삭제하지 않습니다.
API 테스트 명령에는 TypeScript 회귀와 .mjs 데이터·파일 이관 동작 테스트가 모두 포함됩니다. Preview 검증은 HTTP 경로만 대상으로 합니다. Queue consumer·Cron은 Preview에서 실행하지 않으므로 알림·예약의 enqueue를 실행 완료로 보고하지 않습니다. 전환 중 기존 테스트 Worker의 consumer가 제거됐는지도 운영 담당자가 별도로 확인해야 합니다.
Preview D1/R2/Queue는 -test 리소스를 사용하지만 DO namespace는 Preview마다 자동 생성됩니다. 웹 Preview의 SSR·브라우저는 KOIN_API_V2 stage 서버(https://api.stage.koreatech.in)를 사용합니다. 오류 자동화 Preview에 운영 GitHub 토큰이나 운영 저장소 쓰기 권한을 주지 않습니다.
실제 계정·결제·파일·메시지·사용자 데이터에 영향을 주는 테스트를 임의로 실행하지 않습니다. 테스트 Worker·D1·R2를 사용하고, 운영 데이터 이관과 도메인 전환은 별도 승인과 복구 절차를 갖춥니다.
업무 API의 현재 방향은 기존 Java 서버 유지입니다. apps/api의 TypeScript Worker는 레거시로 보존하며 wrangler.jsonc의 API_ENABLED가 운영·Preview 모두 "false"입니다. 코드·D1 데이터·R2·Queue·DO·마이그레이션·테스트 fixture를 정리하거나 삭제하는 PR을 만들지 않습니다. 비활성 전환은 설정값으로만 표현하고, 기존 활성 동작 테스트는 fixture에 API_ENABLED: 'true'를 명시해 유지합니다. 비활성 경계(503 API_DISABLED, scheduled 무동작, Queue retryAll, DO 쓰기 차단) 테스트는 추가할 수 있습니다.
로컬·격리 검증에서 업무 경로가 필요하면 해당 프로세스에만 wrangler dev --var API_ENABLED:true를 넣습니다. scripts/autofix/local-api.mjs가 이 방식을 사용하며 보조 Worker 사본의 vars만 치환합니다. 배포 설정이나 Cloudflare 대시보드 값을 바꾸는 방식으로 테스트를 통과시키지 않습니다. SOURCE_SYNC_ENABLED("false"), triggers.crons([]), 원본 호스트의 비공개 sync-enabled 승인 파일은 기존 게이트이므로 API_ENABLED와 묶거나 중복 게이트를 추가하지 않고, 사용자 재승인 전에는 비활성 상태를 유지합니다.
원격 반영은 저장소 커밋과 별개의 운영 절차입니다. 웹의 Java API 전환 PR #9는 병합됐고 운영 웹 배포도 확인됐지만, 현재 원격 레거시 API 비활성 상태는 별도로 확인해야 합니다. Java 서버 CORS와 웹의 실제 Java API 경로 전환을 확인하기 전에 API_ENABLED: "false"를 원격에 반영하면 남아 있는 레거시 Worker 호출이 장애를 겪습니다. Queue는 코드의 retryAll()로 멈추지 않으므로 배포 전후에 wrangler queues pause-delivery koin-api-jobs를 실제로 실행해야 하며, 저장소 값만으로 원격 비활성이 완료됐다고 보고하지 않습니다. 절차는 README의 레거시 Worker API 비활성화와 복원을 따릅니다.