Sepet
Alışveriş listeni yaz, marketlerdeki en uygun fiyatları karşılaştır ve en ucuz sepeti saniyeler içinde oluştur.
저장소 정보는 아직 수집하지 않았습니다.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
PIXEL MART 2차 — 게임 같은 픽셀아트 데스크 소품 상점 + 동네 덕후 커뮤니티 (Next.js 16 · FastAPI)
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗일상에 아이템을 장착하세요. 게임을 좋아하는 사람을 위한 픽셀아트 게이밍 데스크 소품 쇼핑몰 + 같은 취향의 동네 이웃과 아이템을 주고받는 PIXEL LOCAL
2차 프로젝트 (2026.10.23 ~ 10.28): 1차에서 인수한 PIXEL MART를 FastAPI 4계층 백엔드로 다시 세우고, 네 명이 각자 Use Case 하나를 화면부터 DB까지 끝까지 만들었어요.
자료 위치
| 자료 | 주소 |
|---|---|
| GitHub 저장소 | https://github.com/KANT-2/pixel-mart-v2 |
| 배포 (Vercel + Render) | https://pixel-mart-v2.vercel.app |
| Swagger | 배포 https://pixel-mart-v2.vercel.app/api/docs · 로컬 http://localhost:3000/api/docs |
| CI (GitHub Actions) | https://github.com/KANT-2/pixel-mart-v2/actions/workflows/ci.yml |
| 팀 Notion | (제출 링크 추가 예정) |
| 발표자료 | (제출 링크 추가 예정) |
| 항목 | 내용 |
|---|---|
| 한 줄 | 취향 기반 커뮤니티 커머스: 쇼핑으로 아이템을 "장착"하고, PIXEL LOCAL에서 같은 동네 덕후를 발견해 주고받고, 그 활동이 하트·뱃지로 쌓여 신뢰가 돼요 |
| 대상 | 게임·서브컬처를 좋아하는 20~30대, 책상을 꾸미고 굿즈를 모으는 사람 |
| 콘셉트 | 다크 아케이드 · 8비트 픽셀아트 · 연두 네온 포인트 · 게임 HUD(하트·코인·퀘스트) |
| 쇼핑 | 상품 검색·정렬·필터, 상품 상세(포토 리뷰·문의), 찜, 장바구니, 가상 주문(재고·배송비), 배송지·주소 검색, 쿠폰·적립금, 결제수단, 운송장 조회, 취소·구매 확정·반품/교환 |
| 플레이어 | 이메일 가입·로그인, 비밀번호 변경·재설정, 구글 로그인, 픽셀 아바타, 퀘스트·뱃지 |
| PIXEL LOCAL | 덕력지도 · 위시맵 · 거래·교환 · 이웃 선물 |
| 관리자 | 주문 배송 단계·취소 처리, 운송장·반품/교환, 상품 문의 답변, 회원·거래글·신고·뱃지·상품·공지·FAQ |
| 범위 밖 | 실제 결제, 데스크 꾸미기·토너먼트 |
네 명 모두 풀스택으로, 각자 Use Case 하나를 화면 → API → 도메인 규칙 → DB까지 직접 만들었어요.
| 역할 | 이름 | Use Case | 폴더 | 함께 맡은 일 |
|---|---|---|---|---|
| 🛡️ 기사 | 양성용 (@dydtlsrl) | 장바구니에서 주문하기 (재고 · 배송비) | backend/src/orders/ | 공통 기반(uv · src/ 뼈대 · DB · 마이그레이션) · ERD |
| 🧭 탐험가 | 채희주 (@kixxuya) | 가입 · 로그인 · 로그아웃 (JWT 블랙리스트) | backend/src/auth/ | 모두가 쓰는 CurrentUser · AdminUser, README · Notion 틀 |
| 🏹 궁수 | 우동현 (@donghyunwoo1222) | 관리자 주문 관리 (배송 단계 · 취소 승인/거절) | backend/src/admin_orders/ | /admin 화면 틀 (메뉴 · 문지기) |
| 🔮 마법사 | 김예주 (@kyeju) | 상품 문의 → 관리자 답변 (비밀글) | backend/src/questions/ | 손님 화면 진짜 데이터 연결 |
팀원별 기여 (2026.10.28 origin/main 기준 · 커밋은 인수 커밋 38720a6 이후, GitHub 화면에서 누른 머지 커밋 포함)
| 팀원 | 담당 UC | 커밋 수 | 머지한 PR 수 | 대표 PR |
|---|---|---|---|---|
| 양성용 | 주문하기 (재고 · 배송비) | 56 | 21 | #43 4계층 기반 · #65 주문 트랜잭션 · #81 동시 주문 재고 |
| 채희주 | 가입 · 로그인 · 로그아웃 | 109 | 47 | #49 가입·로그인·로그아웃 서비스 · #50 블랙리스트 로그아웃 · #158 배포 |
| 우동현 | 관리자 주문 관리 | 63 | 27 | #72 단계 넘기기·취소 서비스 · #84 취소 요청 화면 · #171 CSV · 알림 후처리 |
| 김예주 | 상품 문의 → 관리자 답변 | 49 | 23 | #73 문의 서비스 · #77 관리자 답변 API · #78 Q&A 실제 API 연결 |
| 1차 팀 (인수 이전) | — | 361 | — | 황정민 50 · 안중섭 30 · 여름 8 · 채희주 273 (1차 저장소에서 옮겨 온 기록) |
b3matego(이메일이 dydtlsrl noreply) → 양성용 · donghyunwoo1222 · 본인 이름(같은 이메일) → 우동현. 다시 세기: git log 38720a6..origin/main --format='%an' | sort | uniq -c협업 규칙: main 직접 push 금지 → feat/* · docs/* 브랜치 → PR(Closes #이슈) → 머지. 마이그레이션은 기사만 만들어요 (uv run alembic heads가 항상 한 줄).
38720a6 (1차 저장소 KANT-2/pixel-mart의 main을 그대로 옮겨 온 시점)backend/app/: routers · services · models), pip + requirements.txt, 구글 로그인과 개발용 로그인(쿠키 JWT, 로그아웃은 쿠키 삭제만), 관리자 기능은 개발자 API(/api/dev/...)로만| 영역 | 인수 전 (38720a6) | 2차 이후 | 담당 | 상태 |
|---|---|---|---|---|
| 구조 | app/ 한 층에 라우터·서비스·모델 | 새 기능은 src/<기능>/ 4계층, 도메인은 순수 파이썬 | 모두 | ✅ |
| 패키지 · 실행 | pip · requirements.txt · Python 3.12 | uv · pyproject.toml · uv.lock · Python 3.13+ | 🛡️ | ✅ |
| 서버 기반 | 앱 생성만 | lifespan(DB 확인·정리) · 요청 로깅 미들웨어 · 공통 예외 → HTTP | 🛡️ | ✅ |
| DB | 재고·배송비·역할·비밀번호 칸 없음 | products.stock · orders.shipping_fee · users.role · users.password_hash · token_blacklist (마이그레이션 ①) | 🛡️ | ✅ |
| 가입 · 로그인 | 구글 · 개발용 로그인만 | 이메일 가입(bcrypt 해싱) · OAuth2 로그인(/api/auth/token, Swagger Authorize) | 🧭 | ✅ |
| 로그아웃 | 쿠키만 삭제 (복사한 토큰은 7일간 유효) | 토큰 jti를 블랙리스트에 → 같은 토큰은 401 (구글·개발용 로그인 포함) | 🧭 | ✅ |
| 권한 | 로그인 확인만 | CurrentUser(Bearer · 쿠키) · AdminUser(관리자 아니면 404). 기존 API도 같은 검사 | 🧭 | ✅ |
| 관리자 회원 | 없음 | 회원 목록 (검색 · 역할 필터 · 페이지) | 🧭 | ✅ |
| 주문 | app/ 서비스가 재고·배송비 없이 주문 저장 | src/orders/ 4계층으로 재고 차감(품절·부족 409) · 3만 원 이상 무료 배송 · 생성 전체 트랜잭션 · 상태/검색/페이지 목록 | 🛡️ | ✅ |
| 관리자 주문 | 개발자 API(/api/dev/...) | /api/admin/orders · 배송 단계 한 칸씩 · 취소 승인(재고 복구)/거절 + /admin 화면 | 🏹 | ✅ |
| 상품 문의 | 화면은 새로고침하면 사라지는 메모리 데이터 | DB 저장 · 비밀글은 서버에서 가림 · 관리자 답변 1회 · 답변 알림 | 🔮 | ✅ |
| 도전 A · 동시성 | 재고를 읽고 나서 줄임 | 재고는 stock >= 수량 조건부 UPDATE, 같은 사람 동시 주문 · 같은 취소 요청 동시 승인은 행 잠금() + 동시 요청 테스트 |
backend/app/ (인수한 코드) | backend/src/ (2차에서 새로) | |
|---|---|---|
| 무엇 | 쇼핑 · PIXEL LOCAL · 아바타 등 1차 기능 전부 | 4개 Use Case + 공통 기반 |
| 구조 | routers · services · models · schemas | <기능>/presentation · application · domain · infrastructure |
| 앱 | app/main.py는 src.main을 다시 내보내기만 해요 | src/main.py가 앱을 조립 (기존 라우터 + 새 라우터) |
| 로그인 확인 | app/deps.py → src/common/auth.py를 다시 내보냄 | src/common/auth.py (CurrentUser · OptionalUser · AdminUser) |
| DB | app/core/db.py → src/database.py를 다시 내보냄 | src/database.py (비동기 엔진 · 세션 · Base) |
새 기능은 src/에만 만들고, app/은 연결 한 줄씩만 고쳐요. 그래서 기존 API(장바구니 · 주문 등)도 새 인증(블랙리스트)을 그대로 받아요.
backend/src/
├─ main.py 앱 조립: lifespan · CORS · 로깅 · 예외 처리기 · 라우터
├─ database.py 비동기 엔진 · AsyncSession · Base
├─ common/ errors.py(예외 → HTTP) · auth.py(CurrentUser·AdminUser) · pagination.py · logging.py
└─ <기능>/ auth · orders · admin_orders · questions
├─ presentation/ 라우터 · 요청/응답 스키마 (검증 · 호출 · 변환만)
├─ application/ 서비스: 조회 → 도메인 → 저장 → commit (실패하면 rollback)
├─ domain/ 규칙 · 상태 변경 (FastAPI · SQLAlchemy · Pydantic import 0개)
└─ infrastructure/ Repository(ORM ↔ 도메인) · bcrypt · JWT
flowchart LR
B["브라우저 :3000"] -->|"/api/* (Next rewrites)"| R["presentation<br/>라우터 · 스키마"]
R -->|"CurrentUser · AdminUser"| A["common/auth.py<br/>서명 · 만료 · 블랙리스트"]
R --> S["application<br/>서비스 · 트랜잭션"]
S --> D["domain<br/>규칙 (순수 파이썬)"]
S --> I["infrastructure<br/>Repository"]
I --> DB[("PostgreSQL")]
D -. "도메인 예외" .-> E["common/errors.py<br/>401 · 404 · 409 · 422"]
| 실패 | 상태 코드 | 예 |
|---|---|---|
| 입력 형식 · 규칙 | 422 | 비밀번호 7자, 빈 제목 |
| 인증 | 401 | 로그인 안 함, 틀린 비밀번호, 로그아웃한 토큰 |
| 권한 · 남의 것 | 404 | 일반 사용자의 관리자 API, 남의 주문 |
| 규칙 충돌 | 409 | 이미 가입한 이메일, 재고 부족, 두 번 답변 |
입력 형식 오류(422)는 공통 처리기가 "비밀번호: 8자 이상 입력해 주세요."처럼 필드 이름 + 한글 안내로 (필드를 모르면 "입력값을 확인해 주세요. …") 바꾸고, 보낸 입력값(비밀번호 등)을 응답에 다시 담지 않아요 (backend/src/common/errors.py).
준비물: Node.js 22+ · Python 3.13+ · uv · PostgreSQL(Docker Desktop 또는 설치형)
| 실제 사용 버전 | 로컬 (개발 PC) | CI (GitHub Actions) | 기타 |
|---|---|---|---|
| Python | 3.14.7 | 3.13 (uv python install 3.13) | pyproject.toml requires-python >=3.13 · Render 3.13.1 |
| Node.js | 24.19 | 22 | |
| uv | 0.13.5 | astral-sh/setup-uv@v6 | 의존성은 uv.lock 고정 (uv sync --locked) |
| PostgreSQL | 18 (docker-compose.yml) | 16 (서비스 컨테이너) |
git clone https://github.com/KANT-2/pixel-mart-v2.git
cd pixel-mart-v2
# 1) 백엔드 (backend/)
cd backend
uv sync # 가상환경 + 의존성 (uv.lock 그대로)
if (!(Test-Path .env)) { Copy-Item .env.example .env } # 처음 한 번만
# → 8장 DB 준비 후
uv run uvicorn src.main:app --reload --port 8000
# 2) 프론트 (새 터미널, 저장소 루트)
npm ci
npm run dev
/signup에서 이메일 가입, 또는 개발 환경의 개발용 로그인(이메일만). 테스트 계정은 *@pixelmart.test환경 변수 (backend/.env): 실제 값은 저장소에 올리지 않아요 (.env.example에 이름만)
| 이름 | 설명 |
|---|---|
ENV | local이면 개발용 로그인 · 개발자 API 켜짐 |
DATABASE_URL | postgresql+asyncpg://… (Docker는 5433, 설치형은 5432) |
JWT_SECRET | 토큰 서명 비밀값 (아무 긴 문자열) |
FRONTEND_URL | CORS 허용 주소 (http://localhost:3000) |
GOOGLE_CLIENT_ID · GOOGLE_CLIENT_SECRET | 구글 로그인 (비우면 구글 버튼만 꺼짐, 팀 채널로만 공유) |
CLOUDFLARE_ACCOUNT_ID · CLOUDFLARE_API_TOKEN · CLOUDFLARE_IMAGE_MODEL · AI_AVATAR_PER_HOUR | AI 픽셀 아바타 (비우면 AI 만들기만 꺼짐) |
ADMIN_EMAIL | 배포 전용. 시작 스크립트(scripts/render_start.sh)가 이 이메일 계정을 make_admin으로 관리자로 지정 (먼저 가입한 계정, 비우면 건너뜀) |
SHOWCASE_EMAIL | 배포 전용. 이 계정(쉼표로 여러 개)에 발표 시연 데이터(주문 · 리뷰 · 문의 · 선물 · 교환 신청)를 seed_showcase로 채움 (한 번 로그인한 뒤부터, 비우면 건너뜀) |
| 검사 명령 | 위치 |
|---|---|
uv run ruff check . · uv run pytest --ignore=tests/test_seed_demo.py · uv run alembic heads | backend/ |
npm run lint · npx tsc --noEmit · npm test · npm run build | 저장소 루트 |
CI: main으로 가는 PR과 main push마다 GitHub Actions가 위 검사를 자동으로 돌려요 (.github/workflows/ci.yml, PostgreSQL 16 서비스 + alembic upgrade head + seed). tsc가 경로 타입(PageProps) 오류를 내면 npx next typegen을 먼저 실행해요.
빌드 재현성: main의 모든 머지 커밋은 CI에서 lint · tsc · test · build · ruff · alembic · pytest를 통과해야 머지돼요 (저장소 규칙 "main 보호": PR 필수 + frontend · backend 두 검사 필수) → Actions 실행 기록
| 구성 | 서비스 (무료 요금제) | 설정 |
|---|---|---|
| 프론트 | Vercel | 환경 변수 BACKEND_URL = 백엔드 주소 · vercel.json (싱가포르 리전) |
| 백엔드 + DB | Render (Web Service + PostgreSQL) | render.yaml 블루프린트 · 시작 시 backend/scripts/render_start.sh |
render.yaml대로 API 서버와 DB가 만들어져요. 시작할 때마다 alembic upgrade head → seed → seed_demo (재실행 안전)BACKEND_URL = https://<Render 서비스 주소> → DeployFRONTEND_URL = Vercel 주소 → 저장(자동 재시작)ADMIN_EMAIL에 넣으면 재시작 때 관리자가 돼요| 환경 변수 | 배포 값 | 지키지 않으면 |
|---|---|---|
ENV | production | 개발용 로그인 · 개발자 API(/api/dev/...)가 열려요. production이면 둘 다 404 |
JWT_SECRET | Render가 자동 생성 | 기본값이거나 32자 미만이면 서버가 시작을 거부해요 |
DATABASE_URL | Render DB와 자동 연결 | postgres:// 주소는 postgresql+asyncpg://로 자동 변환 |
FRONTEND_URL | Vercel 주소 | 구글 콜백(<FRONTEND_URL>/api/auth/google/callback) · CORS |
(프론트) BACKEND_URL | Render 주소 | /api/* 전달이 안 돼요 |
secure(https 전용), 로그인 화면의 개발용 로그인 칸도 숨겨져요./api/docs · /api/openapi.json)는 production에서 관리자로 로그인한 사람만 열려요. 그 외에는 404# Docker를 쓸 때 (저장소 루트). 설치형 PostgreSQL이면 건너뛰기
docker compose up -d db
# backend/에서
uv run alembic upgrade head # 테이블 만들기 · 최신 마이그레이션
uv run python -m scripts.seed # 상품 · 지역 · 취향 · 키보드 (재실행 안전)
uv run python -m scripts.seed_demo # 선택: 시연용 데모 이웃
uv run python -m scripts.make_admin 이메일 # 관리자 만들기 (먼저 한 번 가입·로그인한 계정)
uv run python -m scripts.purge_blacklist # (가끔) 이미 만료된 로그아웃 토큰 줄 정리 — 만료 전 줄은 남겨서 401 그대로
tests/test_seed_demo.py는 빼고 돌리고, 테스트 뒤 seed_demo를 다시 실행해 주세요.칸마다 흐름 · 관련 파일 · API · 정상/실패 결과 · 캡처 · 인수 전후 비교, 그리고 로그인 → 내 API 200 → 로그아웃 → 같은 토큰 401을 담았어요.
flowchart LR
screen["화면<br/>/cart → /checkout"] --> api["API<br/>POST /api/orders"]
api --> service["Application<br/>OrderService.place_order()"]
service --> domain["Domain<br/>Order · summarize_prices()<br/>StockItem.take() 규칙"]
service --> repo["Infrastructure<br/>OrderRepository"]
repo --> db["PostgreSQL<br/>products · orders · order_items<br/>order_status_history · notifications · cart_items"]
repo -->|"조건부 UPDATE 실패"| rollback["rollback<br/>주문·재고 그대로"]
관련 파일: 화면 components/cart/ · components/orders/CheckoutContents.tsx · 클라이언트 lib/cart.ts · lib/orders.ts · API backend/src/orders/presentation/ · 서비스 backend/src/orders/application/ · 도메인 backend/src/orders/domain/ · 저장소 backend/src/orders/infrastructure/
핵심 코드: 라우터 create_order → 서비스 OrderService.place_order → 도메인 summarize_prices · 배송비 기준 · StockItem.take → 저장소 조건부 UPDATE
jti)를 블랙리스트에 적어 같은 토큰으로는 다시 들어올 수 없게 합니다. 팀원 모두가 쓰는 로그인 확인 부품(CurrentUser · AdminUser)도 이 UC에서 만들었습니다.flowchart LR
screen["화면<br/>/signup · /login"] --> api["API<br/>POST /api/auth/signup · token · logout"]
api --> service["Application<br/>AuthService"]
service --> domain["Domain<br/>Account · IssuedToken<br/>check_password() · normalize_email()"]
service --> infra["Infrastructure<br/>bcrypt hasher · JWT(jti)<br/>UserRepository · TokenBlacklistRepository"]
infra --> db["PostgreSQL<br/>users · token_blacklist"]
guard["common/auth.py<br/>CurrentUser · AdminUser"] -->|"서명 · 만료 · 블랙리스트 확인"| infra
other["다른 팀원 API<br/>주문 · 관리자 · 문의"] --> guard
관련 파일: 화면 components/EmailSignupForm.tsx · components/LoginForm.tsx · components/AuthProvider.tsx · components/LoginRequiredNotice.tsx · 클라이언트 lib/api.ts · API backend/src/auth/presentation/ · 서비스 backend/src/auth/application/ · 도메인 backend/src/auth/domain/ · 저장소·도구 backend/src/auth/infrastructure/ (hasher.py · jwt.py · repositories.py) · 공용 의존성 backend/src/common/auth.py
/admin/orders에서 주문을 주문 완료 → 상품 준비중 → 배송중 → 배송 완료로 한 칸씩 넘기고, /admin/cancels에서 취소 요청을 승인(재고·적립금 복구)하거나 거절(신청 직전 단계로 복귀)합니다. 상태가 바뀔 때마다 구매자에게 알림이 갑니다.flowchart LR
screen["화면<br/>/admin/orders · /admin/orders/[id] · /admin/cancels"] --> api["API<br/>POST /api/admin/orders/{id}/advance<br/>POST /api/admin/cancel-requests/{id}/approve · reject"]
api --> service["Application<br/>AdminOrderService"]
service --> domain["Domain<br/>OrderProgress.advance() 한 칸씩<br/>CancelRequest.approve() · reject() 한 번만"]
service --> repo["Infrastructure<br/>AdminOrderRepository"]
repo --> db["PostgreSQL<br/>orders · order_status_history<br/>cancel_requests · products · notifications"]
repo -->|"취소 처리 중 실패"| rollback["rollback<br/>상태·기록 그대로"]
api -.->|"응답 뒤 BackgroundTasks"| notice["단계 진행 알림<br/>(새 세션 · 실패는 로그만)"]
관련 파일: 화면 app/admin/orders/ · app/admin/cancels/ · components/admin/ (AdminNav · AdminGuard · StatusFilter · ConfirmDialog) · 클라이언트 lib/admin.ts · API backend/src/admin_orders/presentation/ · 서비스 backend/src/admin_orders/application/ · 도메인 backend/src/admin_orders/domain/ · 저장소 backend/src/admin_orders/infrastructure/
핵심 코드: 라우터 advance_order · approve_cancel_request · reject_cancel_request → 서비스 AdminOrderService.advance_order · · → 도메인 · → 저장소
/admin/questions에서 한 번만 답변하고 작성자에게 알림이 갑니다.flowchart LR
screen["화면<br/>상품 상세 Q&A 탭 · /admin/questions"] --> api["API<br/>POST /api/products/{id}/questions<br/>POST /api/admin/questions/{id}/answer"]
api --> service["Application<br/>QuestionService.ask() · answer()"]
service --> domain["Domain<br/>Question · can_be_read_by()<br/>answer_with() 한 번만"]
service --> repo["Infrastructure<br/>QuestionRepository"]
repo --> db["PostgreSQL<br/>product_questions · notifications"]
repo -->|"알림 저장 실패"| rollback["rollback<br/>답변도 저장 안 됨"]
관련 파일: 화면 components/products/QuestionsPanel.tsx · QuestionEntries.tsx · QuestionDeleteButton.tsx · app/admin/questions/ · 클라이언트 lib/questions.ts · API backend/src/questions/presentation/ · 서비스 backend/src/questions/application/ · 도메인 backend/src/questions/domain/ · 저장소 backend/src/questions/infrastructure/
핵심 코드: 라우터 create_question · answer_question → 서비스 QuestionService.ask · answer → 도메인 · → 저장소 ·
| 항목 | 지금 상태 | 구분 | 다음 단계 · 이유 |
|---|---|---|---|
| 결제 | 가상 결제 — 결제수단만 고르고 PG 미연동, 주문·재고·적립금은 실제 DB에 저장 | 실제 API · 결제만 가상 | 발제 제외 범위 |
| 상품 목록 · 상세 · 카테고리 | API 실패 시 lib/products.ts가 data/products.ts · data/categories.ts로 폴백 (백엔드가 꺼져도 쇼핑 기본 화면 동작) | 실제 API + 장애 폴백 | 유지 |
| FAQ | API 실패 시 lib/faqs.ts가 lib/faqFallback.ts(시드와 같은 내용)로 폴백 | 실제 API + 장애 폴백 | 유지 |
mocks/ 폴더 | mocks/admin.ts만 남음. lib/admin.ts는 USE_MOCK = false라 실제 API만 사용, mock은 백엔드 머지 전 화면 개발용 잔여 | 미사용 Mock | 정리 예정 |
| 정적 데이터 | 키캡 · 키보드 · 데스크 룩북 테마(data/keycaps.ts · data/keyboards.ts · data/deskLookbook.ts) | 정적 데이터 (Mock 아님) | 유지 |
| 비밀번호 찾기 | 메일 발송 없음 — 로컬에서는 화면에 재설정 링크를 바로 보여 주고, 배포에서는 "메일 발송 미지원" 안내만 | 메일만 미구현 | 메일 서버 연동 |
레거시 backend/app 라우터 | src/main.py가 아직 app.routers의 health · auth(구글 · 개발용 로그인) · products · faqs · cart · orders.dev_router · (+) · · · · · · · · · · 를 서비스 (인증은 새 로 검사, 는 로컬 전용 · 배포 404) |
| 상황 | 서버 응답 | 화면 |
|---|---|---|
| 입력 형식 오류 | 422 "비밀번호: 8자 이상 입력해 주세요." — 공통 처리기가 한글로 바꾸고 보낸 값(비밀번호 등)은 다시 담지 않음 (errors.py) | 폼 아래 안내 문구 |
| 업무 규칙 위반 | 도메인 예외 → ERROR_STATUS_MAP 한 곳에서 409 · 404 · 400 + 한글 detail | 같은 문구 그대로 |
| 로그인 필요 · 로그아웃한 토큰 | 401 | "로그인이 필요합니다" + 로그인 안내 (lib/api.ts) |
| 남의 데이터 · 일반 사용자의 관리자 API | 404 (있다는 사실도 숨김) | 찾을 수 없음 안내 |
| 예상 못한 서버 오류 | 500 — 스택·SQL은 응답에 없음 | "요청을 처리하지 못했습니다" |
| 화면이 깨짐 · 없는 주소 | — | 공통 GAME OVER 화면: error.tsx · global-error.tsx (레이아웃까지 깨질 때) · not-found.tsx + 상품 · 주문 · 공지 상세 404 |
서버 로그에는 메서드 · 경로 · 상태 · 처리 시간만 남기고, 422는 필드 이름과 오류 종류만 남겨요 (입력값 · 토큰 · 쿼리스트링 X).
sub · jti · exp(7일)만 — 개인정보는 넣지 않음. 매 요청 CurrentUser가 서명 · 만료 · 블랙리스트 확인jti를 token_blacklist에 저장 → 같은 토큰 401. 만료된 줄은 scripts.purge_blacklist로 정리AdminUser가 관리자 역할 확인, 서비스가 "내 주문 · 내 문의인지" 확인 — 아니면 404pm_session은 HttpOnly · SameSite=Lax · 배포에서 Secure/api/dev/*는 배포에서 404 · CORS는 FRONTEND_URL만 허용 · .env는 git 제외= + - @로 시작하는 칸은 엑셀 수식으로 실행되지 않게 처리| 종류 | 무엇을 | 예 |
|---|---|---|
| 도메인 단위 | DB 없이 규칙만 (배송비 · 상태 전이 · 비밀번호 규칙) | test_orders_domain.py · test_admin_orders_domain.py |
| 서비스 · 롤백 | 중간에 실패하면 주문 · 재고 · 알림이 모두 되돌아가는지 | test_orders_service.py · test_admin_orders_service.py · test_questions_service.py |
| API | 정상 · 422 · 404 · 409 · 401(로그아웃 토큰) 응답과 문구 | test_auth_api.py · test_orders_api.py · test_error_messages.py |
| 동시성 | 같은 사람의 동시 주문이 한 건만 되는지 | test_orders_duplicate_race.py |
| 프론트 | utils/ 순수 함수 (가격 표시 등) | tests/*.test.mjs (268개) |
백엔드 pytest 720여 개 · 프론트 268개를 PR마다 CI가 새 PostgreSQL에서 돌리고, 통과해야만 main에 머지돼요. 직접 돌리기: backend/에서 uv run pytest --ignore=tests/test_seed_demo.py, 루트에서 npm test.
| 이름 | 역할 | 담당 |
|---|---|---|
| 채희주 (@kixxuya) | 프론트엔드 | 화면 전체, 디자인 시스템, 메인·상품·PIXEL LOCAL UI |
| @skshdwkdvkfl55-art | BE-A 플랫폼·커머스 | 구글 로그인, 장바구니, 찜, 아바타, PIXEL LOCAL(덕력지도·거래) |
| @totoriri605-oss | BE-B 주문·CS | FAQ, 가상 주문·배송, 취소, 리뷰, 상품 Q&A, 키캡 호환성 |
| @kyrdufmal-summer | BE-C → 기획·정책 | PIXEL LOCAL 서비스 정의·운영 정책·개인정보 기준 정리(이슈 #151~), 현재 구현·화면 구조 문서화 |
| 구분 | 사용 기술 |
|---|---|
| 프론트 | Next.js 16 (App Router, Turbopack, Cache Components) · React 19 · TypeScript 5 · Tailwind CSS 4 (@theme 디자인 토큰) |
| 백엔드 | FastAPI · SQLAlchemy 2 (async) · asyncpg · Alembic · Pydantic v2 · PyJWT · bcrypt · Authlib(구글 OAuth) · uv |
| DB | PostgreSQL (Docker 또는 설치형) |
| 테스트 | pytest (백엔드) · node --test (프론트 유틸) · ESLint · ruff |
| 외부 | Cloudflare Workers AI (AI 픽셀 아바타, 사용자 동의 시에만) |
| 폰트 | Pretendard (본문) · Galmuri (픽셀 타이틀) |
| 협업 | Git · GitHub (GitHub Flow + Pull Request · 이슈·라벨) · Notion |
| 경로 | 페이지 | 주요 내용 | 상태 |
|---|---|---|---|
/ | 메인 | 배너 3장(내 캐릭터 · 월드맵 · 데스크 룩북), 추천 아이템 | ✅ |
/products | 전체 상품 | 검색·정렬·필터·등급, 무한 스크롤 | ✅ |
/products/[id] | 상품 상세 | 이미지·정보·장바구니, 상품정보·리뷰·문의 탭, 같은 카테고리, 최근 본 아이템, 없는 id 안내 | ✅ |
/products/[id]/fitting | 키캡 피팅룸 | 내 키보드에 끼워 보기, 조합 장바구니 담기 | ✅ |
/lookbook | 데스크 테마 룩북 | 테마별 데스크와 상품 | ✅ |
/cart · /checkout | 장바구니 · 주문하기 | 수량·합계, 배송지 입력, 가상 주문 | ✅ |
/login · /signup | 로그인 · 가입 | 이메일·비밀번호 로그인/가입(2차), 구글 로그인 + 닉네임 정하기, 개발용 로그인 | ✅ |
/mypage | 마이페이지 | 닉네임, 아바타, 뱃지 도감, 바로가기 | ✅ |
/mypage/orders · /[id] | 주문 내역 · 상세 | 배송 단계·타임라인, 취소 신청, 리뷰 쓰기 | ✅ |
/mypage/wishlist · /gifts · /avatar · /quests | 찜 · 선물함 · 픽셀 아바타 · 내 퀘스트 | 찜 목록, 받은/보낸 선물(받기·거절), AI·캔버스 아바타, 퀘스트 요약 | ✅ |
/local | 덕력지도 | 시 › 구 › 생활권 지도, 취향 필터, 지역 패널, 이웃 카드 | ✅ |
/local/wish-map | 위시맵 | 동네별 WISH 글, 닉네임·아이템 검색, 선물하기 | ✅ |
/local/trades · /new · /mine | 거래·교환 | 게시판, 글쓰기, 내 글·교환 신청함·매칭 | ✅ |
/local/gift | 이웃에게 선물하기 | 수량·메시지, 데모 결제 | ✅ |
/local/settings | 내 동네·취향 설정 | 동네·취향(최대 20), 공개 설정 5종 | ✅ |
/help | 고객센터 | FAQ 검색·분류 | ✅ |
| 404 · 오류 | GAME OVER 화면 | 없는 페이지·없는 상품·오류 | ✅ |
flowchart LR
main["메인"] --> list["전체 상품"] --> detail["상품 상세"] --> cart["장바구니"] --> order["주문 · 배송"]
detail -->|키캡| fit["키캡 피팅룸"] --> cart
main -->|월드맵| map["덕력지도"] --> card["이웃 카드"]
map --> trades["거래·교환"] -->|교환 신청 · 수락| trust["하트 · 뱃지 · 알림"]
card --> wish["위시맵"] -->|선물하기| gift["선물 → 받는 사람 주소 → 주문"] --> trust
화면 구조·컴포넌트 트리·흐름 5개 상세는 docs/UI_STRUCTURE.md 에 있습니다.
| 메인 (데스크톱) | 메인 (모바일) |
|---|---|
| 전체 상품 (무한 스크롤) | 상품 상세 |
|---|---|
| 키캡 피팅룸 | 데스크 룩북 |
|---|---|
| PIXEL LOCAL 덕력지도 | 덕력지도 (모바일) |
|---|---|
| 거래·교환 | 위시맵 (선물하기) |
|---|---|
캡처는 로그인하지 않은 상태, 1280px·390px 화면 기준입니다.
| 문서 | 내용 |
|---|---|
| docs/UI_STRUCTURE.md | 화면 구조 기획과 분할 · 컴포넌트 조립과 배치 · 사용자 흐름 |
| docs/PIXEL_LOCAL_AS_BUILT.md | PIXEL LOCAL 현재 구현 (화면 문구·규칙·알려진 한계·시연 순서) |
| docs/archive/TASTE_COMMERCE_PLAN.md | (보관) 10/12 기준 다음 단계 기획 (취향 세분화·하트·개인정보·레퍼런스 분석) |
| docs/PIXEL_LOCAL_POLICY.md · docs/DUKRYEOK_MAP_POLICY.md · docs/AVATAR_POLICY.md | 운영 정책 |
| docs/PRODUCT_STRATEGY.md · docs/UX_CX_STRATEGY.md | 상품·UX 전략 |
| docs/backend/ | 백엔드 실행·구조·스키마·API |
| AGENTS.md · docs/SETUP.md · docs/archive/ | 작업 규칙 · 환경 세팅 · 보관 문서 |
| 문제 | 원인 | 해결 |
|---|---|---|
동적 상세 페이지의 params 처리 | params는 Promise이며, Cache Components 환경에서는 동적 값을 읽는 위치도 고려해야 함 | 페이지는 params를 자식에 전달하고, <Suspense> 내부의 ProductDetail에서 await params로 읽음. generateStaticParams와 notFound()로 정적 경로·없는 상품도 처리 |
| 모바일에서 로고 · 메뉴가 두 줄로 꺾임 | 헤더 가로 공간 부족 | whitespace-nowrap, 모바일 여백 축소 |
ESLint가 <img> 사용을 경고 | Next.js는 next/image를 권장 | 과제 권장에 따라 <img> 유지, 사유를 단 주석으로 처리 |
FOR UPDATE| 🛡️ 🏹 |
| ✅ |
| 도전 B · 스트리밍 · 후처리 | 없음 | 관리자 주문 CSV 내려받기(StreamingResponse, 목록과 같은 필터, 엑셀 한글 BOM, 수식 주입 방지) · 배송 단계 알림은 응답 뒤 BackgroundTasks | 🏹 | ✅ |
| 도전 C · 배포 | 로컬에서만 실행 | Vercel(프론트) + Render(API · PostgreSQL), 환경 변수 분리 · 배포에서 개발용 기능 404 | 🧭 | ✅ |
take_stockDB로 확인 (psql로 접속, 화면에서 주문하기 전·후 실행 · 먼저 \set uid 내사용자id · \set pid 상품id)
SELECT id, stock FROM products WHERE id = :pid; -- 주문 전 재고
SELECT id, status, total_price, shipping_fee FROM orders WHERE user_id = :uid ORDER BY id DESC LIMIT 1;
SELECT product_id, quantity, unit_price FROM order_items WHERE order_id = (SELECT max(id) FROM orders WHERE user_id = :uid);
SELECT id, stock FROM products WHERE id = :pid; -- 주문 후 재고 = 전 − 수량
SELECT count(*) FROM cart_items WHERE user_id = :uid; -- 0 (장바구니 비움)
재고보다 많이 담고 주문하면 409 재고 부족 → orders 새 줄 없음 · stock 그대로 · 장바구니 그대로 (rollback). 같은 규칙의 테스트: tests/test_orders_service.py::test_out_of_stock_rolls_back_order_other_stock_and_cart · tests/test_orders_api.py::test_out_of_stock_is_409_and_keeps_cart
업무 규칙: 상품 합계 30,000원 미만은 배송비 3,000원, 이상은 무료 배송 · 재고 차감은 stock >= quantity 조건부 UPDATE · 주문 생성 중 하나라도 실패하면 전체 rollback · 내 주문만 조회하며 남의 주문도 같은 404로 숨김
API
| 메서드 · 경로 | 정상 | 주요 실패 |
|---|---|---|
POST /api/orders | 주문 확정 201 | 입력 422 · 빈 장바구니/재고 부족 409 · 로그아웃 토큰 401 |
GET /api/orders?status=&q=&page=&size= | 상태 필터·상품명/주문번호 검색·페이지 목록 200 | 로그아웃 토큰 401 |
GET /api/orders/{id} | 내 주문 상세·타임라인 200 | 없거나 남의 주문 404 · 로그아웃 토큰 401 |
Swagger 정상 / 실패 증거
로그인 → 내 API → 로그아웃 → 같은 토큰 401
인수 전후: 재고·배송비 없이 상품 합계만 저장하던 주문 → 재고 차감·3만 원 무료 배송·검색/페이지 조회·오류 계약을 갖춘 4계층 주문으로 개선했고, 실패 시 주문과 재고가 함께 원상 유지됩니다.
트러블슈팅: 공유 시드 상품 재고를 바꾸지 않도록 문서 검수 전용 상품·계정을 사용했고, 캡처에서는 Swagger의 Bearer 토큰이 보이는 Curl 영역을 제외했습니다.
signupDB로 확인 (가입 → 로그인 → 로그아웃 뒤 실행)
SELECT id, email, role, left(password_hash, 7) AS hash_head FROM users WHERE email = '내이메일@pixelmart.test'; -- $2b$12$ (bcrypt, 평문 없음)
SELECT jti, expires_at, created_at FROM token_blacklist ORDER BY id DESC LIMIT 1; -- 방금 로그아웃한 토큰
같은 토큰으로 GET /api/auth/me → 401. 두 번 로그아웃해도 token_blacklist는 한 줄 (jti UNIQUE). 중복 가입은 409 → users 새 줄 없음. 같은 규칙의 테스트: tests/test_auth_api.py::test_bearer_login_me_logout_then_same_token_is_401 · test_logout_twice_and_without_token_is_fine
업무 규칙: 비밀번호는 8자 이상 · 72바이트 이하(bcrypt 한계)이고 해시로만 저장 · 이메일은 앞뒤 공백 제거 + 소문자로 비교 · 로그인 실패는 "없는 이메일"과 "틀린 비밀번호"를 구분하지 않음 · 토큰은 Bearer 헤더를 먼저, 없으면 pm_session 쿠키 확인 · 로그아웃한 jti는 만료 시각까지 블랙리스트에 보관(같은 토큰 401, 두 번 로그아웃해도 한 줄) · 관리자가 아니면 관리자 API는 404로 숨김
API
| 메서드 · 경로 | 정상 | 주요 실패 |
|---|---|---|
POST /api/auth/signup | {email, password, nickname} 가입 201 | 비밀번호 8자 미만 422 · 중복 이메일/닉네임 409 |
POST /api/auth/token | OAuth2 form(username=이메일 · password) → {access_token, token_type} + 쿠키 200 | 틀린 이메일·비밀번호 401 "이메일 또는 비밀번호가 맞지 않아요." |
GET /api/auth/me | 내 정보 + role 200 | 로그아웃 토큰 401 "다시 로그인해 주세요." |
POST /api/auth/logout | jti를 블랙리스트에 · 쿠키 삭제 200 | 없음 (두 번 해도 200) |
GET /api/admin/users?q=&role=&page=&size= | 관리자 회원 목록(검색 · 역할 필터 · 페이지) 200 | 일반 사용자 404 · 로그아웃 토큰 401 |
Swagger 정상 / 실패 증거
로그인 → 내 API → 로그아웃 → 같은 토큰 401 (Swagger Authorize로 Bearer 토큰을 붙이고, 브라우저 쿠키는 지운 상태)
테스트: tests/test_auth_domain.py(규칙, DB 없음) · tests/test_auth_service.py(가짜 저장소로 commit/rollback, 실제 DB로 블랙리스트 중복·정리) · tests/test_auth_api.py(Bearer · 쿠키 · 개발용 로그인 토큰 모두 로그아웃 후 401, 관리자 API 404/200) · tests/test_admin_users.py
인수 전후: 구글 · 개발용 로그인만 있고 로그아웃은 쿠키 삭제뿐이라 복사해 둔 토큰이 7일간 유효 → 이메일 가입(bcrypt) · OAuth2 로그인 · jti 블랙리스트로 로그아웃 즉시 같은 토큰 401. 구글 · 개발용 로그인 토큰과 기존 app/ API도 같은 검사를 받습니다.
트러블슈팅: OAuth2 form 로그인은 python-multipart가 있어야 동작 → uv add로 추가 · bcrypt 5.0이 72바이트 넘는 비밀번호에서 오류 → 요청 검사에서 72바이트 제한 · /api/auth/signup 경로가 기존 구글 닉네임 가입과 겹침 → 구글 쪽을 /api/auth/google/signup으로 이동 · 캡처는 테스트 전용 계정(*@pixelmart.test)으로 찍고 비밀번호·토큰 값은 가렸습니다.
approve_cancelDB로 확인 (\set oid 주문id · \set rid 취소요청id, 화면에서 단계 넘기기 · 승인/거절 뒤 실행)
SELECT id, status FROM orders WHERE id = :oid; -- paid → preparing → …
SELECT status, changed_at FROM order_status_history WHERE order_id = :oid ORDER BY id; -- 타임라인
SELECT id, status FROM cancel_requests WHERE id = :rid; -- approved / rejected
SELECT p.id, p.stock FROM products p JOIN order_items i ON i.product_id = p.id WHERE i.order_id = :oid; -- 승인 시 수량만큼 복구
SELECT kind, title, created_at FROM notifications WHERE user_id = (SELECT user_id FROM orders WHERE id = :oid) ORDER BY id DESC LIMIT 3;
배송 완료 주문을 또 넘기거나 같은 요청을 다시 승인하면 409 → status · 타임라인 · stock 그대로 (재고 두 번 복구 없음). 같은 규칙의 테스트: tests/test_admin_cancels_api.py::test_approve_twice_is_409_and_does_not_restore_stock_again · tests/test_admin_orders_service.py::test_approve_cancel_rolls_back_everything_when_notify_fails
업무 규칙: 배송 단계는 정해진 순서로 한 칸씩만 (배송 완료·취소된 주문은 더 못 넘김 409) · 취소 요청은 한 번만 처리 (두 번째 409) · 승인하면 주문 취소 + 재고 복구 + 쓴 적립금 환불 · 거절하면 타임라인에서 취소 요청 직전 단계로 복귀 · 취소 승인 · 거절은 상태 · 타임라인 · 구매자 알림을 한 트랜잭션으로 · 단계 넘기기 알림은 응답을 먼저 보낸 뒤 BackgroundTasks로 (알림이 실패해도 상태 변경은 유지, 로그만 남김) · 관리자가 아니면 404로 숨김
API
| 메서드 · 경로 | 정상 | 주요 실패 |
|---|---|---|
GET /api/admin/orders?status=&q=&page=&size= | 상태 필터 · 주문번호/닉네임/이메일 검색 · 페이지 200 | 일반 사용자 404 · 로그아웃 토큰 401 |
GET /api/admin/orders/{id} | 주문 상세 · 타임라인 200 | 없는 주문 404 |
POST /api/admin/orders/{id}/advance | 다음 단계로 200 → 응답 뒤 구매자 알림 | 더 못 넘기는 상태 409 · 없는 주문 404 |
GET /api/admin/orders/export.csv?status=&q= | 목록과 같은 필터의 주문 CSV (스트리밍, 엑셀 한글 BOM) | 일반 사용자 404 · 로그아웃 토큰 401 |
GET /api/admin/cancel-requests?status=&page=&size= | 취소 요청 목록 200 | 일반 사용자 404 |
POST /api/admin/cancel-requests/{id}/approve · reject | 승인(취소·재고 복구) / 거절(직전 단계로) 200 | 이미 처리 409 · 없는 요청 404 |
Swagger 정상 / 실패 증거
로그인 → 내 API → 로그아웃 → 같은 토큰 401 (Swagger Authorize로 관리자 Bearer 토큰을 붙이고, 브라우저 쿠키는 지운 상태 · 로그인 뒤 200은 위 주문 목록 캡처)
화면
테스트: tests/test_admin_orders_domain.py(단계 전이·처리 규칙, DB 없음) · tests/test_admin_orders_service.py(실패 시 rollback) · tests/test_admin_orders_api.py · tests/test_admin_cancels_api.py(관리자만 · 409 · 재고 복구)
인수 전후: 로컬에서 로그인한 누구나 부르던 개발자 API(/api/dev/orders/{id}/advance · /api/dev/cancel-requests/...) → 관리자만 쓰는 /api/admin/... + 허용된 상태 전이만 + 관리자 화면(대시보드 · 주문 목록/상세 · 취소 요청)
트러블슈팅: 백엔드 머지 전에는 mocks/admin.ts 임시 데이터로 화면을 먼저 만들고, 머지 후 실제 API로 전환(USE_MOCK = false) · 취소 사유에 공백 없는 긴 글이 들어오면 표 전체가 가로로 늘어남 → 인라인 요소에는 max-width가 적용되지 않아 블록 요소로 바꿔 줄바꿈 · 캡처에서는 Bearer 토큰이 보이는 Curl 영역을 제외
Question.can_be_read_byDB로 확인 (\set uid 작성자id, 문의 쓰기 → 관리자 답변 뒤 실행)
SELECT id, product_id, title, is_secret, answer, answered_at FROM product_questions WHERE user_id = :uid ORDER BY id DESC LIMIT 1;
SELECT kind, title, href, created_at FROM notifications WHERE user_id = :uid ORDER BY id DESC LIMIT 1; -- 답변 알림
같은 문의에 다시 답변하면 409 → answer · answered_at은 첫 답변 그대로, 알림 저장이 실패하면 답변도 rollback. 같은 규칙의 테스트: tests/test_questions_service.py::test_second_answer_is_409_and_keeps_first · test_failed_notification_rolls_back_answer
업무 규칙: 제목 1100자 · 내용 1500자 · 답변 1~500자 (앞뒤 공백 제외) · 비밀글은 작성자와 관리자만 내용을 보고, 다른 사람에게는 서버가 제목·내용·닉네임을 null로 가림 · 답변은 한 번만 (두 번째 409) · 답변이 달린 문의는 삭제할 수 없음 (409, 대화 기록 보존) · 남의 문의 삭제와 일반 사용자의 관리자 API는 404로 숨김 · 답변 저장과 작성자 알림은 한 트랜잭션
API (기존 → 이번)
| API | 인수 당시 (app/routers/questions.py) | 이번 (src/questions/) |
|---|---|---|
GET /api/products/{id}/questions | 목록 · 페이지 (size 기본 10) | + answered 필터 · size 기본 20 · 비밀글은 작성자·관리자도 봄 (#30) |
POST /api/products/{id}/questions | 쓰기 (라우터·서비스에 규칙이 섞임) | 같은 주소 · 규칙은 Question 도메인 · 실패 시 rollback (#30) |
DELETE /api/questions/{id} | 없음 | 새로 · 본인만 · 답변 전에만 (남 404 · 답변 후 409) (#30) |
GET /api/admin/questions?answered=&q=&page= | 없음 | 새로 · 관리자만 · 답변 여부 필터 · 상품 이름/제목 검색 · 비밀글 내용도 보임 · productName·authorNickname (#31) |
POST /api/admin/questions/{id}/answer | POST /api/dev/questions/{id}/answer — 로컬에서 로그인한 누구나 · 두 번 답변 400 | 관리자만(아니면 404) · 두 번 답변 409 · 작성자 알림 · 개발용 API 삭제 (#31) |
Swagger 정상 / 실패 증거
로그인 → 내 API → 로그아웃 → 같은 토큰 401
화면
인수 전후: 새로고침하면 사라지는 메모리 데이터 → DB 저장 · 비밀글은 화면이 아니라 서버에서 가림 · 로그인한 누구나 쓰던 개발용 답변 API → 관리자만 · 관리자 답변 화면 새로 · 함께 메인 하트·코인(#34)과 상품 카테고리 개수(#35)도 하드코딩 → API 값으로 연결
트러블슈팅: 동시에 main.py를 고친 팀원 PR과 충돌 → 두 include_router 줄을 모두 남겨 해결 · Windows 앱 제어가 alembic.exe를 막음 → uv run python -m alembic으로 실행 · 캡처 이미지가 git add -A에 섞여 커밋됨 → git rm --cached 후 .git/info/exclude에 등록 (PowerShell echo는 인코딩이 달라 Set-Content -Encoding ascii 사용) · 캡처에서는 Bearer 토큰이 보이는 Curl 영역을 제외
cancel_requestsdev_routerusersai_avatargiftswishlistlocallocal_tradestrade_requestsreviewscompatnotificationsCurrentUserdev_router| 실제 API (1계층) |
다음 단계: 기능별로 src/ 4계층 이전 |
| 시드 | lifespan은 DB 연결 확인 · 종료만, 시드는 uv run python -m scripts.seed로 따로 실행 (재실행 안전, on_conflict) | 실제 DB | 배포 시작 스크립트와 CI에서 같은 순서(alembic upgrade head → seed)로 명시 실행 |
| 구글 로그인 · AI 아바타 | 키가 없으면 버튼만 꺼짐 (.env 값 필요) | 실제 연동 · 키 필요 | 유지 |
| 알려진 문제 | 처음 clone한 뒤 npx tsc --noEmit이 PageProps 오류를 내면 npx next typegen을 먼저 (경로 타입 생성) | — | — |