Files
chocoadmin/AGENTS.md
T

16 KiB

당신이 알고 있는 Next.js가 아닙니다

이 버전에는 파괴적 변경이 포함되어 있습니다 — API, 관례, 파일 구조가 학습 데이터와 다를 수 있습니다. 코드를 작성하기 전에 반드시 node_modules/next/dist/docs/ 안의 관련 가이드를 읽으세요. 사용 중단(deprecation) 안내도 놓치지 마세요.


프로젝트: chocoadmin

chocomae 서비스(mouse-typing)의 관리자 패널을 독립 Next.js 16 앱으로 분리한 프로젝트입니다. 기존 MariaDB 데이터베이스를 공유합니다 — DB 스키마를 변경하지 마세요.

필수 규칙

  • DB 스키마를 절대 변경하지 마세요. 모든 테이블은 운영 중인 chocomae 서비스와 공유됩니다.
  • 승인/취소/거절 경합 방지: 트랜잭션 내부의 첫 단계로 조건부 updateMany({ where: { id, Status: REQUEST_STATUSES.REQUESTED }, data: { Status: <target> } })를 사용하세요. count === 0이면 존재 여부를 조회해 404 또는 409를 반환합니다. 읽고-쓰기(findUnique → 상태 확인 → update) 방식은 절대 사용하지 마세요 — 동시 요청 시 중복 처리가 발생합니다.
  • 모든 변경은 트랜잭션으로 감쌉니다: maestro, maestro_extension/maestro_upgrade, maestro_log는 원자적으로 함께 갱신되어야 합니다.
  • 이메일은 커밋 성공 후에만 발송하며, 트랜잭션 내부에서 절대 발송하지 마세요.
  • maestro_extension이나 maestro_upgrade를 조회할 때 include: { maestro: true }를 절대 사용하지 마세요. 항상 select로 필요한 컬럼만 지정해 maestro의 Password 필드가 로그에 남지 않도록 합니다.
  • 관리자 암호 검증은 MariaDB의 PASSWORD() 함수를 사용합니다 — 기존 admin 테이블은 이 방식으로 해시된 암호를 저장합니다. 마이그레이션 계획 없이는 변경하지 마세요.
  • 연장 승인 만료일 계산은 서버 사이드에서 이루어집니다: 현재 만료일이 과거이면 오늘 + 1년, 그렇지 않으면 현재 만료일 + 1년.
  • 업그레이드 승인은 AvailableActivateDateTime = NOW() + 1 year로 설정합니다. 업그레이드 시 PlayerCount를 변경하지 마세요.
  • 연장 또는 업그레이드를 승인할 때는, 동일한 MaestroID의 다른 Status = 1 요청들을 모두 Status = 2로 닫은 뒤 승인 대상 요청을 Status = 3으로 설정합니다.
  • Docker 컨테이너는 TZ: Asia/Seoul을 설정해야 합니다 — 그래야 날짜 계산(setHours(0,0,0,0), formatDate)이 공유 MariaDB의 KST 기반 기존 데이터와 일치합니다.
  • Docker 서비스 이름은 환경별로 고유해야 합니다: Docker는 공유 네트워크에서 각 services: 키를 DNS 별칭으로 등록합니다. 스테이지 서비스는 chocoadmin-stage입니다 — chocoadmin으로 이름을 바꾸지 마세요. 이름이 동일하면 proxy-network에서 DNS 라운드 로빈이 발생하며, NPM의 set $server "chocoadmin"이 요청을 두 컨테이너 사이로 무작위 라우팅해 운영과 스테이지 트래픽이 섞입니다.
  • 서버 액션은 반드시 모듈 최상위 named export여야 합니다: 인라인 action={async () => { "use server"; ... }} 클로저는 빌드마다 새 액션 ID를 얻습니다. 재배포 후 브라우저가 오래된 ID를 보내면 Next.js는 Failed to find Server Action으로 응답합니다. 항상 별도의 actions.ts 파일(예: app/(admin)/actions.ts)에 최상위 export로 추출하세요.
  • 재배포 후 NPM nginx를 재로드하세요: NAS에서 docker exec npm nginx -s reload를 실행해 업스트림 DNS 캐시를 비워야 합니다. 이 단계를 빠뜨리면 NPM이 이전 컨테이너 IP로 계속 라우팅할 수 있습니다.
  • 연장 신청(POST /api/maestros/[id]/extension-requests)은 ActivateStatus = 2(ACTIVE) 또는 1(TRIAL)을 허용합니다. PENDING(0)과 CANCELED(100)은 400으로 거부합니다. AccountType은 제한하지 않습니다 — 어떤 유형(유료 티어 또는 체험 티어)이든 연장 신청 가능합니다. 승인(PATCH /api/extension-requests/[id])도 동일하게 ACTIVE와 TRIAL을 허용하며, 체험 계정은 승인 시 ACTIVE로 승격됩니다. PENDING/CANCELED는 409를 던집니다(원자 클레임 트랜잭션이 롤백됨).
  • 업그레이드 신청: 대상(requestedAccountType)은 반드시 유료 티어(1~5)여야 합니다. 현재 AccountType이 체험 티어(100/101)이면 어떤 유료 티어로도 업그레이드 가능합니다. 현재 AccountType이 유료 티어이면, TRIAL 계정(ActivateStatus = 1)은 동일 티어 이상(체험→유료 전환, requestedAccountType >= AccountType), ACTIVE 계정은 엄격히 상위 티어(>)만 가능합니다. 승인 시점에 이 규칙이 마에스트로의 현재 AccountType을 기준으로 재검증되며, 유효하지 않게 된 경우(예: 관리자가 직접 티어를 올린 경우) 승인은 409를 던지고 롤백됩니다.
  • 관리자 주도 연장/업그레이드 신청 등록은 이메일을 발송하지 않습니다(셀프 등록 흐름은 결제 안내 이메일을 발송함). 이는 의도된 동작입니다.

인증

  • Credentials Provider를 사용하는 NextAuth.js v5(next-auth@5.0.0-beta.31).
  • 설정은 app/ 내부가 아닌 프로젝트 루트의 auth.ts에 있습니다.
  • 미인증 요청은 proxy.ts(미들웨어)에서 잡혀 /login으로 리다이렉트됩니다.
  • API 라우트는 auth()를 호출하고 세션이 없으면 401을 반환해야 합니다.
  • 세션 maxAge는 8시간입니다 — 검토 없이 늘리지 마세요.
  • auth.ts는 관리자 이름별 로그인 실패 횟수를 인메모리 Map으로 추적합니다. 연속 5회 실패 시 15분 동안 잠금되며, 성공 시 카운터가 초기화됩니다.
  • 세션 쿠키 이름 chocoadmin-${APP_ENV}.session-tokenauth.ts(cookies.sessionToken.name)와 proxy.ts(getToken({ cookieName })) 양쪽 모두에 설정되어야 합니다. auth.ts에만 설정하면 proxy.ts가 기본 쿠키 이름을 찾지만 그 이름은 절대 기록되지 않아 → 미들웨어가 모든 요청을 /login으로 리다이렉트 → 로그인이 다시 /로 리다이렉트 → 무한 ERR_TOO_MANY_REDIRECTS가 발생합니다. NextAuth v5에서는 JWT 암호화 salt가 쿠키 이름과 동일하므로, 두 곳이 정확히 일치해야 합니다.
  • 쿠키 이름에 __Secure- 접두사를 붙이지 마세요. Next.js 16 미들웨어는 (Edge가 아닌) Node.js 런타임에서 실행되며 Nginx Proxy Manager로부터 HTTP를 받습니다 — __Secure- 쿠키는 HTTP에서 조용히 거부되어 동일한 리다이렉트 루프를 만듭니다.

핵심 파일

파일 용도
auth.ts NextAuth 설정 (Credentials Provider, MariaDB PASSWORD 검증)
proxy.ts 미들웨어 — 미인증 사용자를 /login으로 리다이렉트
lib/db.ts SQL 프리티 로깅 + 결과 표 로깅이 포함된 Prisma Client 싱글톤
lib/logger.ts 구조화 로거 — debug/info/warn/error, 레벨은 LOG_LEVEL / APP_ENV로 제어
lib/errors.ts ApiError — HTTP 상태 코드가 포함된 통합 에러 클래스
lib/api-handler.ts withApiHandler — 라우트 핸들러를 인증·타이밍·에러 로깅으로 감쌈
lib/constants.ts 상태 코드 상수 (ACTIVATE_STATUS, ACCOUNT_TYPE, REQUEST_STATUS); PAID_ACCOUNT_TYPE_VALUES (유료 계정 유형 배열 — 중복 정의 방지용 공용 상수)
lib/maestros.ts Maestro DB 쿼리
lib/extension-requests.ts 연장 신청 쿼리 및 승인 로직
lib/upgrade-requests.ts 업그레이드 신청 쿼리 및 승인 로직
prisma/schema.prisma prisma db pull로 생성됨 — 모델 필드를 직접 편집하지 마세요
docs/business-rules.md 기존 chocomae 비즈니스 로직의 전체 참조
docs/phase/phase9.md 로그 정책: 레벨, 형식, 마스킹 규칙, Docker 로그 운영

maestro_log 관례

DB 변경이 성공한 뒤에는 반드시 로그 항목을 기록합니다. 다음 Type 값을 사용하세요:

Type 사용 시점
extension_maestro 연장 승인
cancel_extension_maestro 연장 취소 (새 타입, 원본에는 없음)
upgrade_maestro 업그레이드 승인
reject_upgrade_maestro 업그레이드 거절 (새 타입, 원본에는 없음)
update_maestro_name Maestro 이름 변경
update_maestro_email Maestro 이메일 변경
update_maestro_status ActivateStatus 변경 (관리자, 새 항목)
update_maestro_account_type AccountType 변경 (관리자, 새 항목)
update_maestro_available_date AvailableActivateDateTime 변경 (관리자, 새 항목)
update_maestro_allow_enter_code AllowEditEnterCode 변경 (관리자, 새 항목)
request_extension_maestro 관리자 주도 연장 신청 생성
request_upgrade_maestro 관리자 주도 업그레이드 신청 생성
reset_maestro_password 관리자가 Maestro 암호를 123456으로 초기화

Remark 컬럼은 char(100)입니다 — 값을 100자 이내로 유지하세요.

구현된 페이즈 (재구현 금지)

  • 페이즈 0–8: 로그인, 마에스트로 목록/상세, 연장 신청, 업그레이드 신청, Docker 셋업, 트랜잭션 안전성, 인증 가드.

  • 페이즈 9 (대부분 완료): 구조화 로깅(lib/logger.ts), withApiHandler 래퍼(lib/api-handler.ts), ApiError 클래스(lib/errors.ts), Prisma SQL 프리티 로깅 및 결과 표 로깅(lib/db.ts). 남은 항목: 상관관계 ID, 스테이지/운영 로그 검증.

  • 페이즈 10: 마에스트로 정보 수정 폼(PATCH /api/maestros/[id]) + 서버사이드 페이지네이션 학생 목록(GET /api/maestros/[id]/students).

  • 페이즈 11: 연장/업그레이드 승인 후 Nodemailer 자동 이메일 발송(lib/mail.ts). 업그레이드 이메일의 엣지 케이스(체험→유료, 유료→유료) 검증은 아직 미완료.

  • 페이즈 12: 마에스트로 상세 화면에서 관리자 주도 연장/업그레이드 신청 등록.

    • POST /api/maestros/[id]/extension-requests — 서버 사이드에서 읽어온 마에스트로의 현재 AccountType을 사용해 maestro_extension에 삽입.
    • POST /api/maestros/[id]/upgrade-requests — 트랜잭션 내부에서 maestro_upgrade에 삽입; requestedAccountType > maestro.AccountType을 검증.
    • ExtensionRequestsSection.tsx, UpgradeRequestsSection.tsx — 인라인 액션 UI를 가진 클라이언트 컴포넌트; 성공 시 router.refresh() 호출.
    • PAID_ACCOUNT_TYPE_VALUESlib/constants.ts로 통합 — lib/maestros.ts, lib/extension-requests.ts, lib/upgrade-requests.ts, 새 API 라우트 두 개가 공유.
  • 페이즈 15:

    • MaestroEditForm.tsx암호 초기화 버튼 추가 — POST /api/maestros/[id]/reset-password를 호출하며, 마에스트로 이름과 함께 window.confirm을 표시하고 성공/에러 메시지를 인라인으로 노출.
    • lib/maestros.tsresetMaestroPassword(maestroID) 추가 — $transaction 안에서 UPDATE maestro SET Password = PASSWORD('123456')을 실행하며 reset_maestro_password 로그 항목을 남김.
    • app/api/maestros/[id]/reset-password/route.tswithApiHandler로 감싼 POST 핸들러.
    • 배포 셸 스크립트 추가: scripts/deploy-stage.shscripts/deploy-production.sh — 각각 git pull, proxy-network 확보, docker compose up -d --build --remove-orphans, docker exec npm nginx -s reload를 실행. 운영 스크립트는 yes 확인이 필요.
  • 페이즈 13 (보안/정확성 수정):

    • P0-1: lib/extension-requests.tsgetActionTarget을 인라인 select로 교체(include: { maestro: true } 제거) — 디버그 결과 로그에 Password가 노출되지 않도록 함.
    • P0-2: lib/extension-requests.tslib/upgrade-requests.ts 양쪽의 승인/취소/거절 흐름에서 원자적 updateMany({ Status: REQUESTED → target }) 가드를 사용; count === 0이면 404/409를 트리거 — 동시 요청에 의한 중복 처리 제거.
    • P0-3: docker-compose.ymldocker-compose.stage.ymlTZ: Asia/Seoul 설정; lib/maestros.ts의 날짜 변경 로그 remark는 toISOString().slice(0,10) 대신 formatDate()를 사용해 KST 일관성 확보.
    • P1-1/P1-2: createExtensionRequestapproveExtensionRequestActivateStatus === ACTIVE를 요구; UpgradeRequestsSectioncreateUpgradeRequest는 TRIAL 동일 티어 업그레이드를 허용.
    • P1-3: approveUpgradeRequest는 승인 시점에 마에스트로의 현재 AccountType을 기준으로 업그레이드 방향을 재검증.
    • P1-4/P1-5: createExtensionRequestdb.$transaction으로 감싸고, 두 create 함수 모두 request_extension_maestro / request_upgrade_maestro 로그 항목을 기록.
    • P1-6: 연장/업그레이드 승인 라우트는 이제 이메일 발송을 await하며 응답에 emailSent: boolean을 포함; 테이블 UI는 emailSent === false일 때 인라인 앰버 경고를 표시.
    • P2-2: updateMaestro는 이제 availableActivateDateTime을 파싱하지 못하면 조용히 건너뛰는 대신 400을 반환.
    • P2-5: withApiHandler의 catch 블록은 재던지기(HTML 응답을 만들었음) 대신 { message: "Internal server error" } JSON 500을 반환.
    • P2-6: NextAuth 세션 maxAge를 8시간으로 설정(auth.ts).
    • P2-7: auth.ts에 인메모리 무차별 대입 방어 추가 — 5회 실패 → 관리자 이름별 15분 잠금.
    • P2-8: 연장/업그레이드 신청 목록의 기본 status 필터를 undefined(전체)에서 REQUEST_STATUSES.REQUESTED로 변경; 리셋 링크도 ?status=1로 업데이트.
  • 페이즈 16: 반응형 웹 디자인 (Responsive Web Design)

    • app/(admin)/nav-config.ts — navItems 배열 공유 설정 파일 (layout.tsx·MobileNav.tsx 공용)
    • app/(admin)/MobileNav.tsx — 모바일 슬라이드인 사이드바 클라이언트 컴포넌트. 햄버거 버튼(md:hidden), 반투명 오버레이(탭하면 닫힘), X 버튼, ESC 키, pathname 변경 시 자동 닫힘, body 스크롤 잠금 포함. CSS translate-x 트랜지션으로 애니메이션.
    • app/(admin)/layout.tsx — 헤더에 <MobileNav /> 통합. 데스크탑 <aside>(hidden md:block)는 그대로 유지.
    • ExtensionRequestsSection.tsx, UpgradeRequestsSection.tsx<section>overflow-hidden 추가. SSR props로 표가 초기 렌더에 포함되어 min-w-[560px]가 섹션을 밀어내던 문제 수정. 헤더를 flex-col sm:flex-row로 변경해 모바일에서 액션 버튼 접근성 확보.
    • 페이지네이션 <nav> 5개 파일(extension-requests/page.tsx, upgrade-requests/page.tsx, maestros/page.tsx, StudentsSection.tsx, LogsSection.tsx) — self-end sm:self-auto 추가로 모바일에서 오른쪽 정렬.

계획된 페이즈 (미구현)

이후 작업 후보는 docs/plan/project-plan.md Section 10 "MVP 이후 확장 후보" 참조.

알아둘 패키지

  • mariadb + @prisma/adapter-mariadb — Prisma는 mysql2가 아닌 mariadb 드라이버를 직접 사용
  • @base-ui/react — shadcn 컴포넌트에서 Radix UI 프리미티브 대신 사용
  • zod v4 — Zod v4의 API는 v3와 다름; 스키마 작성 전 문서 확인
  • next-auth v5 beta — v4와 API 표면이 크게 다름