chore(dev): W3-2 테스트용 dev seed + serena memory

리뷰/댓글 기능 수동 테스트를 위한 멱등 dev seed.

- seed-dev.sql / seed-dev-teardown.sql: 로그인 테스트 계정 1, 더미 게임 1,
  별점 다양한 더미 리뷰 5(유저별 1리뷰 unique 충족), 댓글 3. NOT EXISTS 가드로 재실행 안전.
- serena memory: PBKDF2(BCrypt 아님) 해시 형식, dev-seed blueprint(FK순서+active-unique),
  JDK 컨테이너 교차검증 패턴 기록.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tHAb6XYHWDPzb82FNKugo
This commit is contained in:
이정수 2026-06-18 14:35:33 +09:00
parent e98c437765
commit 3a06b39d76
8 changed files with 656 additions and 1 deletions

View File

@ -0,0 +1,224 @@
---
schema_version: 2
sid: 20260618-121419
started_at: 2026-06-18T12:14:19+09:00
ended_at: 2026-06-18T13:05:00+09:00
applied_changes:
- "Serena memory 기록: bibimbap-auth-pbkdf2-not-bcrypt (수용, signal=observation)"
- "Serena memory 기록: bibimbap-dev-seed-blueprint (수용, FK순서+active-unique 통합)"
- "Serena memory 기록: java-crypto-verify-via-jdk-container-when-no-host-jdk (수용, signal=positive)"
- "docs_sync 보류: CLAUDE.md / verification-strategies.md 반영은 tracked 문서 편집+미커밋이라 사용자 승인 대기"
user_request: |
새로 추가한 기능(게임 리뷰/댓글, W3-2) 테스트하기 위한 더미 상황이 필요하다:
1. 게임 (dummy game)
2. 계정 (dummy account, 로그인 가능)
3. 이미 등록되어 있는 더미 리뷰
status: done
---
# Summary
W3-2 게임 리뷰/댓글 기능 테스트용 더미 상황을 dev 스키마에 구축. 멱등 seed SQL
(`db/seed-dev.sql`) + 정리 SQL(`db/seed-dev-teardown.sql`) 작성 후 가동중 컨테이너
(bibimbap-db)에 적용. 구성: 로그인 가능 테스트 계정 1개(tester@bibimbap.local /
test1234!), 표시되는 더미 게임 1개(테스트 계정 소유), 별점 다양한 더미 리뷰 5개
(각자 다른 더미 유저 — game당 user 1리뷰 unique 제약 충족), 보너스 댓글 3개.
로그인 계정은 해당 게임에 리뷰가 없어 "직접 작성" 테스트도 가능.
핵심 제약 발견: 로그인 password_hash 는 BCrypt 가 아닌 자체 PBKDF2
(`pbkdf2_sha256$210000$<saltB64>$<hashB64>`, PBKDF2WithHmacSHA256/256bit/16B salt).
저장 해시가 실제 Java `UserController.verifyPassword` 경로로 검증됨을 JDK 컨테이너에서
교차 실행 확인(verify('test1234!')=true, 오답=false) → 로그인 동작 보장.
# Invocations
- research-advisor: 인증 seed 경로 + 리뷰/댓글 유저귀속 + seed 적용경로 조사 (완료)
- artifact: research/index.md (file:line 근거 포함). 결론 = orchestrator 독립 조사와 일치.
- graph-refresh-checker: seed SQL 추가 후 graph staleness 판정 (running)
# Decisions
- 적용 방식: 재사용 SQL 파일(db/seed-dev.sql) + 즉시 적용. (사용자 선택, 추천값)
- 로그인 자격: 기본값 tester@bibimbap.local / test1234! / 닉네임 '테스터'. (사용자 선택)
- 규모: 게임 1 + 리뷰 5(별점 5/4/3/5/2). (사용자 선택)
- 설계: 리뷰어는 별도 더미 유저 5명(unique 제약 game당 user 1리뷰 때문). 로그인 계정은
그 게임에 리뷰 없음(작성 테스트 가능). 게임 소유자 = 로그인 계정(getGamesByUserId 도 테스트).
- design/implementation-advisor 스킵: 스키마·계약·컬럼값 완비된 단일 seed SQL → orchestrator 직접 구현.
- 댓글 3개 보너스 포함: 신규 기능이 리뷰/댓글 동시 배치라 같은 더미 상황에 포함이 합리적.
# Advisor Invocation Decision Log
- advisor: requirements-advisor
decision: skip
rationale: '요청 3항목 명확, seed 위치/자격/규모는 orchestrator 가 plan-gate(AskUserQuestion)로 직접 수렴'
checked_at: 2026-06-18T12:14:30+09:00
- advisor: research-advisor
decision: call
rationale: '로그인 가능 계정 seed 위한 password_hash 알고리즘·provider·세션귀속이 미문서화 미지수'
checked_at: 2026-06-18T12:15:00+09:00
- advisor: design-advisor
decision: skip
rationale: 'research 로 컬럼·계약·제약 전부 확정 → 설계 오픈질문 0, orchestrator 직접 설계'
checked_at: 2026-06-18T12:30:00+09:00
- advisor: implementation-advisor
decision: skip
rationale: '단일 SQL 파일 산출, 파일 충돌 없음 → worker 분산 불요'
checked_at: 2026-06-18T12:31:00+09:00
- advisor: verification-advisor
decision: skip-direct
rationale: 'orchestrator 가 매퍼 동등쿼리 + JDK 교차해시검증 + 멱등 재실행을 직접 실행해 AC 충족 확인'
checked_at: 2026-06-18T12:45:00+09:00
# user_signals
positive: []
negative: []
# verified_by_me
- L1 (데이터 무결성 / 매퍼 동등 쿼리, 실 DB 실행):
- login identity 조회(provider=email): tester 1건 + password_hash 존재 → pass
- tester users.status=ACTIVE (로그인 필수 조건) → pass
- getVisibleGames 동등: '테스트 게임 (더미)' is_visible=true 포함 → pass
- listGameReviews(game): 5건, 최신순 정렬 + authorName JOIN 정상 → pass
- getActiveReviewByGameAndUser(game, tester): 0건 → 본인 리뷰 작성 테스트 가능 → pass
- listGameComments(game): 3건 → pass
- L2 (교차구현 해시 검증, 실제 Java verifyPassword 코드 경로, eclipse-temurin:21-jdk):
- verify('test1234!', 저장해시) = true → 로그인 성공 보장
- verify('wrongpass', 저장해시) = false → 음성 케이스 정상
- 멱등성: seed-dev.sql 재실행 후 카운트 불변(users6/games1/reviews5/comments3) → pass
- 로그 스캔: psql ON_ERROR_STOP=1 무에러, NOTICE 정상 → clean
# needs_user_verification
- 런타임 UI 스모크 1회 (호스트에 JDK 없음 + bibimbap-app 미가동이라 자동 불가):
1. `docker compose up --build` (또는 호스트 실행) 으로 앱 기동
2. tester@bibimbap.local / test1234! 로 로그인 → 성공 확인
3. '테스트 게임 (더미)' 상세 진입 → 기존 리뷰 5건 + 댓글 3건 표시 확인
4. 로그인 상태로 리뷰 1건 + 댓글 1건 신규 작성 → 정상 등록 확인
# graph_refresh
- 판정: partial-stale (but 본 세션 변경은 fresh).
- 본 세션 변경(db/seed-dev.sql, seed-dev-teardown.sql)은 src/docs graph scope 대상경로
밖 데이터 픽스처 → graph 영향 0, 재생성 불요(no-defer: 본 세션 무액션).
- partial-stale 트리거는 **별도 작업단위**(W3-2 댓글/리뷰 기능 구현 — GameCommentController/
GameReviewController/GameReviewsMapper/GameReviewData 등, 대부분 untracked). 해당 코드가
미커밋 진행중이라 지금 /graphify src/ 재생성은 시기상조 → open_items 로 권고만 이월하지
않고 "기능 커밋 시점 처리" 로 명시.
# post-session-debug (로그인 server error 후속)
- 사용자 보고 "로그인 시 server error(500)". 진단 결과:
- 원인 = **stale 앱 인스턴스**(pid 59109, 6/17 14:36 기동, 최근 auth 수정 이전 빌드).
내 seed 가 tester 계정을 만들어 처음으로 "계정 존재" 상태 로그인 경로를 타며 표면화.
- 조치: 사용자 동의(직접 캡처 요청) 하에 pid 59109 종료 → `JAVA_HOME=/opt/homebrew/opt/openjdk@21
./mvnw -P dev spring-boot:run` 재기동(현재 코드 = target/classes 재컴파일, W3-2 컨트롤러 포함).
로그: /tmp/bibimbap-app.log. 신규 pid 23318 (백그라운드).
- 결과: `POST /login` 200, `GET /game/3/reviews` 200(5건), `GET /game/3/comments` 200(3건),
`GET /game/3` 200. 더미 상황 런타임 정상. → needs_user_verification 의 UI 스모크 사실상 완료.
- **발견 버그 (별도 작업단위 = W3-2 댓글기능, 내 seed 무관)**: 댓글 목록 author 항상 null.
- 근거: GameCommentsMapper.listGameComments 가 `nickname AS authorName` 로 alias →
결과 컬럼 `authorName` 인데 GameCommentData 엔 `nickname` 프로퍼티만 존재 → 미매핑 →
commentView(GameCommentController.java:179) `getNickname()` null.
- 대조: 단건 getGameComment 는 `nickname`(alias 없음) → 정상.
- 1줄 수정안: listGameComments 의 `nickname AS authorName``nickname`.
- 처리: 사용자 요청 시에만 (in-progress 기능 코드라 무단 수정 보류).
# open_items
- 미커밋 신규 파일: db/seed-dev.sql, db/seed-dev-teardown.sql (작업단위 산출물).
프로젝트/하니스 커밋정책 = 사용자 요청 시에만 커밋 → 커밋 보류, 사용자 확인 대기.
- (별도 작업단위) W3-2 댓글/리뷰 기능 코드 커밋 시 `/graphify src/` 재생성 + docs/graph/index.md
frontmatter(source_commit, last_generated_at) + Scopes 표 src 행 갱신 권고. 본 더미데이터
task 범위 밖.
# Retrospective
Retrospective:
signals:
positive:
- quote_or_paraphrase: "추천 옵션 전부 수락 (seed 적용방식·자격·규모 plan-gate 선택지 그대로 채택)"
about: "orchestrator 의 plan-gate(AskUserQuestion) 추천값 + research 선행으로 advisor 다수 스킵한 경량 경로"
- quote_or_paraphrase: "재지시·재호출 없이 단일 패스로 수렴 (부정 시그널 0)"
about: "research 1회로 컬럼·계약·제약을 전부 확정 → design/impl/verification advisor 스킵한 판단이 검증됨"
negative: []
what_went_well:
- "research-advisor 선행 1회로 인증 메커니즘(PBKDF2)·유저귀속·seed 적용경로를 모두 확정해, design/implementation/verification advisor 3종을 근거 있게 스킵하고 orchestrator 직접 구현으로 수렴. advisor invocation decision log 에 skip rationale 가 항목별로 남아 사후 추적 가능."
- "호스트에 JDK 가 없는 제약에도 password_hash 를 추정·방치하지 않고 eclipse-temurin:21-jdk 컨테이너에서 실제 verifyPassword 코드 경로로 교차 실행(positive=true / negative=false)해 '로그인 가능' AC 를 자기검증으로 닫음. self-report 가 아닌 실행 증거."
- "schema.sql 이 '비권위 복원본'이라는 메타 한계를 research concerns 에 보존하고, 컬럼명은 매퍼로 교차확인하되 제약·타입은 추정으로 표기해 신뢰도 과대평가를 회피."
- "seed SQL 을 멱등 + teardown 쌍으로 작성하고 재실행 후 카운트 불변을 실증해 반복 적용 안전성을 확보."
- "graph staleness 를 partial-stale 로 정직하게 판정하되, 본 세션 변경(데이터 픽스처)은 graph scope 밖임을 구분해 불필요한 재생성을 회피하고 잔여 stale 은 별도 작업단위로 정확히 귀속."
what_to_improve:
- "런타임 UI 스모크는 호스트 환경 제약(JDK 없음 + 앱 미가동)으로 자동 불가 → needs_user_verification 으로 정확히 이월됨. 개선이라기보다 환경 한계의 정직한 핸드오프. (구조적 결함 아님)"
- "seed SQL 의 제약/타입/default 는 schema.sql 비권위 유래라 실 DB `\\d` 대조까지는 미수행(행수·테이블존재·매퍼 동등쿼리로만 검증). 운영 DB 와 default 가 다르면 재현 시 불일치 여지 — 현 dev 스키마에선 무영향이나 운영 적용 시 유의."
memory_candidates:
- name: bibimbap-auth-pbkdf2-not-bcrypt
type: reference
description: "bibimbap 로그인 해시는 BCrypt 가 아닌 자체 PBKDF2 — 로그인 가능 더미/테스트 계정 seed 시 이 포맷으로 직접 생성해야 함"
body_draft: |
# bibimbap 인증: PBKDF2 자체 해시 (BCrypt 아님)
Spring Security 미사용. 로그인은 UserController 자체 구현 password 검증.
password_hash 포맷: `pbkdf2_sha256$210000$<base64(salt)>$<base64(hash)>` ($ 4필드)
- 알고리즘 PBKDF2WithHmacSHA256, iterations=210000, keyLength=256bit, salt=16byte SecureRandom
- 검증 경로: UserController.verifyPassword (parts[0]=="pbkdf2_sha256" && len==4 확인 후 동일 iter/salt 재계산, MessageDigest.isEqual 상수시간 비교)
## Why
외부 BCrypt 생성기로 만든 해시는 무효 — 형식 자체가 다름. 향후 로그인 가능 더미/테스트/관리자 계정을
seed 할 때마다 이 함정에 반복적으로 부딪힌다. 코드로 유도는 가능하나 "BCrypt 일 것"이라는 기본 가정이
강해 매번 재확인 비용이 든다.
## How to apply
- 로그인 가능 계정 seed 시 위 PBKDF2 스펙으로 해시 문자열을 직접 생성.
- 로그인 동작 보장은 실제 verifyPassword 코드 경로(JDK 컨테이너 교차 실행 등)로 positive/negative 둘 다 확인.
- 로그인 필수 조건 동봉: users.status='ACTIVE', user_auth_identities.provider='email',
provider_user_id=정규화(trim+lowercase) 이메일, 양쪽 is_delete=false.
rationale_for_saving: "코드로만 드러나는 비자명 사실(BCrypt 아님)이고, 테스트/더미 계정 seed 마다 재발하는 함정. schema/git log 로는 '자체 PBKDF2 라서 외부 생성기 무효'라는 함의가 드러나지 않음."
signal_source: observation
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/CLAUDE.md
- name: bibimbap-seed-blueprint-fk-and-unique
type: reference
description: "dev 스키마 더미 seed 청사진 — FK 순서 + game_reviews active-unique 제약(게임당 user 1리뷰)"
body_draft: |
# bibimbap dev 더미 seed 청사진
현실적 경로 = dev 스키마에 psql 직접 INSERT (UI 경유는 CSRF+세션+WebGL 업로드 강제로 고비용).
DB 직삽은 CSRF·세션·중복선검사를 우회하되 FK·CHECK·유니크idx 는 그대로 적용.
INSERT 순서(FK 의존):
1. users (status='ACTIVE' 필수, role='USER', is_delete=false, display_name 권장)
2. user_auth_identities (PBKDF2 password_hash — 별도 memory 참조; provider='email')
3. games (user_id FK, name; is_visible/is_delete default 로 목록노출 충족; webgl/thumbnail NULL 가능)
4. game_reviews (game_id, user_id, rating 1~5, body)
5. game_comments (game_id, user_id, nickname[직접 채움 — 조회가 users JOIN 안 함], content)
## 핵심 제약
- game_reviews 부분 유니크 idx `(game_id, user_id) WHERE is_delete IS NOT TRUE`
→ 한 게임에 더미 리뷰 N개 = 더미 유저 N명 필요 (한 user 는 게임당 활성 1리뷰).
- game_comments 는 유니크 없음 → 자유 다수. 작성자명 보이려면 nickname 직접 채움(비정규화 저장).
- 게임 목록+상세 노출 = games.is_visible=true, games.is_delete=false, 연결 users.is_delete=false.
## Why
리뷰 N개를 같은 유저로 넣으려다 유니크 위반으로 막히는 게 첫 시도의 흔한 실패. schema.sql 이
'비권위 복원본'이라 제약을 코드에서 직접 읽기 전엔 드러나지 않음.
## How to apply
- seed SQL 확정 직전 `\d dev.<table>` 로 제약/타입/default 운영 대조 권장(schema.sql 비권위).
- 리뷰 다양화가 목표면 더미 유저를 리뷰 수만큼 선행 INSERT.
rationale_for_saving: "seed 설계 시 반복되는 유니크-위반 함정 + FK 순서. schema.sql 비권위라 코드 교차확인 없이는 유도 불가하고, 더미/픽스처 작업마다 재사용되는 청사진."
signal_source: observation
docs_sync_target: null
- name: java-crypto-verify-via-jdk-container-when-no-host-jdk
type: feedback
description: "호스트에 JDK 없을 때 Java 암호/검증 로직을 eclipse-temurin:21-jdk 컨테이너 single-file 실행으로 교차검증"
body_draft: |
# 호스트 JDK 부재 시 Java 로직 교차검증 패턴
호스트에 JDK 가 없고 앱도 미가동인 환경에서, password_hash 같은 Java 암호/검증 로직의
정합성을 추정으로 닫지 않고 `eclipse-temurin:21-jdk` 컨테이너에 single-file/javac 로
실제 코드 경로(예: verifyPassword)를 옮겨 실행해 positive+negative 케이스를 모두 확인.
## Why
seed 한 해시가 "로그인 된다"는 self-report 는 증거가 아니다. 실제 검증 함수와 동일한
알고리즘/iter/salt 로 재계산해 true/false 가 나와야 AC 가 닫힌다. 호스트 toolchain 부재는
검증 생략의 이유가 되지 않고, 컨테이너로 우회 가능하다.
## How to apply
- 검증 대상 메서드의 알고리즘 파라미터(iter/keyLength/salt/encoding)를 코드에서 그대로 추출.
- 동일 로직을 단일 .java 로 재현해 temurin 컨테이너에서 실행, 정답=true / 오답=false 둘 다 확인.
- 런타임 UI 스모크처럼 컨테이너로도 불가한 부분만 needs_user_verification 으로 이월.
rationale_for_saving: "환경 제약(호스트 JDK 없음) + Java 검증 로직이라는 비자명 조합. self-report 대신 실행 증거로 AC 를 닫는 재현성 있는 검증 워크플로우 — 단발 우발 아님."
signal_source: positive
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md
protocol_feedback: []
applied_changes: []

View File

@ -0,0 +1,149 @@
---
phase: research
agent: research-advisor
agent_version: 2
generated_at: 2026-06-18T03:18:28Z
concerns:
- "low source confidence — db/schema.sql 은 헤더 주석상 '비권위 복원본'(users/user_auth_identities/game_comments). 컬럼명·매퍼 일치는 코드로 교차확인됐으나 타입·길이·기본값·제약은 추론값. 운영 DB 제약과 다를 수 있으므로 seed SQL 확정 전 실 DB(\\d 조회)로 컬럼·제약 대조 필요."
- "BCrypt 가 아닌 PBKDF2 자체 해시이므로 seed용 password_hash 문자열은 외부 BCrypt 생성기로 만들 수 없음. iterations=210000/HmacSHA256/16byte-salt/256bit 스펙에 맞춰 직접 생성해야 함(생성 스크립트 자체는 design/구현 몫)."
concerns_checked: true
source_confidence: high
workers_spawned: 3
---
# 조사 결과
## 주제
bibimbap(Java 21 / Spring Boot / JSP / MyBatis, WAR) 에 테스트용 더미 데이터를 seed 하기 위한 사전 조사.
"로그인·리뷰 작성이 실제로 동작하는" 형태로 (1) 게임 (2) 로그인 가능 계정 (3) 더미 리뷰+댓글 을 만들기 위한
정확한 컬럼값·메커니즘 도출. 코드 수정 없음.
> 신뢰도 표기: `확인됨` = 코드/실행으로 1차 확인. `추정` = 통용·유추. `미확인` = 검증 불가.
> 본 조사의 사실 항목은 전부 프로젝트 코드 직접 확인 기반 → `source_confidence: high`.
> 다만 schema.sql 자체가 "비권위 복원본"이라는 메타 한계는 concerns 에 보존(컬럼명은 매퍼로 교차확인됨, 제약·타입은 추론).
---
## 포인트별 발견
### 포인트 A: 로그인/인증 메커니즘 (실제 로그인 가능한 계정 seed)
- 경로: `src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java`,
`data/UserAuthIdentityData.java`, `mapper/UserAuthIdentitiesMapper.java`, `mapper/UsersMapper.java`, `db/schema.sql`
요약:
- **Spring Security 미사용.** `PasswordEncoder|SecurityFilterChain|@EnableWebSecurity|spring-boot-starter-security`
src/ + pom.xml 0 hit. 인증은 UserController 자체 구현 password 로그인. **OAuth 없음**(google/kakao/naver/oauth 0 hit). [확인됨]
- 로그인 엔드포인트: `POST /login`, form 파라미터 `email`,`password`,`remember`(선택) — UserController.java:122, 125-128. [확인됨]
진입부에서 `CsrfTokens.isValid(request)` 실패 시 403 — UserController.java:131-133. [확인됨]
- **provider 컬럼 값은 `"email"` 단 하나.** `PROVIDER_EMAIL="email"` — UserController.java:42.
identity 생성 시 set — UserController.java:314. 조회 필터도 동일 값 — UserController.java:140-143. [확인됨]
`provider_user_id` 에는 **정규화 이메일**(trim+lowercase) 저장 — UserController.java:104-106, 527-533. [확인됨]
- **password_hash 알고리즘 = PBKDF2 (BCrypt 아님).** PasswordEncoder 빈 없음. 형식:
`pbkdf2_sha256$<iterations>$<base64(salt)>$<base64(hash)>` (`$` 4필드) — UserController.java:554-556. [확인됨]
- iterations=210000 (UserController.java:46), keyLength=256bit (:47), salt=16byte SecureRandom (:48,551-552),
알고리즘 `PBKDF2WithHmacSHA256` via JCE SecretKeyFactory/PBEKeySpec (:576-584). [확인됨]
- 검증: `verifyPassword()``$` split → parts[0]=="pbkdf2_sha256" && length==4 확인 후 동일 iterations/salt 재계산,
`MessageDigest.isEqual` 상수시간 비교 — UserController.java:144, 559-574. [확인됨]
- PBKDF2 표준 동작(iterations·salt 가 해시 문자열에 동봉되어 검증 시 재현 가능) — [추정/일반 통용 지식]
- **로그인 성공 시 세션 저장** (saveLoginSession — UserController.java:162, 502-525). 세션 직전 `changeSessionId()` 세션고정방어 — :159-160. [확인됨]
개별 attribute: `id`(Long), **`userId`(Long — 인증가드 sessionUserId() 가 읽는 키, :337)**, `displayName`, `email`(canonicalEmail),
`avatarUrl`, `role`, `status`, `authProvider`("email"), `authIdentityId`, `lastLoginAt` — UserController.java:503-512.
추가로 `account` 키에 위 값들의 LinkedHashMap 통째 저장 — :514-524. (UserData 객체 자체가 아니라 개별 스칼라+Map) [확인됨]
- **로그인 검증 컬럼 흐름** [확인됨]:
1. user_auth_identities `WHERE provider='email' AND provider_user_id=<정규화이메일> AND is_delete IS NOT TRUE` — UserAuthIdentitiesMapper.java:47-49
2. password_hash != null && PBKDF2 통과 — UserController.java:144
3. users `WHERE id=<identity.user_id> AND is_delete IS NOT TRUE` — UsersMapper.java:25-26
4. **user.status == "ACTIVE"** 필수 (STATUS_ACTIVE=:44) — UserController.java:148-149
- **seed 시 필수 채움 컬럼** (schema.sql 비권위 — 컬럼명만 신뢰):
- users: `role`(default 'USER'), **`status`='ACTIVE'**(ACTIVE 아니면 로그인 거부), `is_delete`=false. display_name/canonical_email 권장. — schema.sql:30-42 [확인됨, 제약값은 추정]
- user_auth_identities: `user_id`(FK), `provider`='email', `provider_user_id`=정규화이메일(소문자), **`password_hash`=PBKDF2형식**(null이면 즉시거부), `is_delete`=false. — schema.sql:49-63 [확인됨, 제약값은 추정]
- 주의: active-unique idx `ux_user_auth_identities_provider_user_id_active` = `(provider, provider_user_id) WHERE is_delete IS NOT TRUE` — schema.sql:66-68. 같은 이메일 활성 identity 중복 INSERT 실패. [확인됨, idx는 비권위 schema.sql]
- 신뢰도: 확인됨 (코드 직접 확인). schema.sql 유래 제약값만 추정.
### 포인트 B: 리뷰/댓글 작성 시 유저 귀속 + INSERT 컬럼 + CSRF
- 경로: `controller/api/GameReviewController.java`, `data/GameReviewData.java`, `mapper/GameReviewsMapper.java`,
`controller/api/GameCommentController.java`, `data/GameCommentData.java`, `mapper/GameCommentsMapper.java`,
`security/CsrfTokens.java`, `db/schema.sql`, `docs/game-reviews-ddl.sql`
요약:
- **리뷰 API**: GET `/game/{id}/reviews`(목록, :41-42), GET `/game/{id}/reviews/{reviewId}`(:58-62),
POST `/game/{id}/reviews`(작성, :74), PUT(수정, :124), DELETE(소프트삭제, :170) — GameReviewController.java. [확인됨]
작성 바디(@RequestParam, form): `rating`(String→1~5 파싱, 범위밖 400), `body`(최대 1000자) — :78-101, BODY_MAX=:30. [확인됨]
- **user_id 출처 = 세션 attribute `"userId"`** (요청 바디 아님). `sessionUserId(session)``session.getAttribute("userId")` — GameReviewController.java:86, 241-257(:245). 없으면 401. `review.setUserId(userId)` — :109. [확인됨]
- **game_reviews INSERT 컬럼 = 정확히 4개**: `(game_id, user_id, rating, body)` `#{}` 바인딩 — GameReviewsMapper.java:72-86. [확인됨]
- game_id ← PathVariable(:108), user_id ← 세션(:109), rating ← 1~5 정수(:110; DB CHECK BETWEEN 1 AND 5 — schema.sql:129, docs/game-reviews-ddl.sql:49-53), body ← trimToEmpty(빈입력시 ""; DB text nullable) — :98,275-278. [확인됨]
- 미포함(전부 DB DEFAULT): id(seq), created_at/updated_at(now()), is_delete(false), deleted_at(null) — schema.sql:124-127. [확인됨, 비권위 schema]
- 참고: updated_at > created_at 이면 조회 시 edited=true 표시 — GameReviewsMapper.java:24, GameReviewController.java:210. [확인됨]
- **ux_game_reviews_game_user_active** = 부분 유니크 인덱스 `(game_id, user_id) WHERE is_delete IS NOT TRUE` — schema.sql:132-133, docs/game-reviews-ddl.sql:58-61. [확인됨]
의미: 활성 리뷰는 (game,user) 조합당 1개. soft-delete 행은 제외 → 재작성 허용. 코드도 INSERT 전 중복 선검사 후 409 — GameReviewController.java:103-105, GameReviewsMapper.java:56-70. [확인됨]
**seed 주의**: 한 게임에 더미 리뷰 여러 개 원하면 각 행 user_id 를 다르게. 한 user 는 게임당 활성 1개. [확인됨]
- **댓글 API**: GET `/game/{id}/comments`(:39), POST(작성, :56), PUT(:102), DELETE(소프트삭제, :143). 작성 바디 `content`만(최대 200자, CONTENT_MAX=:28) — GameCommentController.java:60,75-78. [확인됨]
- user_id ← 세션 `"userId"` (:67,198). [확인됨]
- **nickname 비정규화 저장 확정**: 작성 시점 세션 `"displayName"` 을 nickname 컬럼에 복사 — `comment.setNickname(authorName)` :84, sessionDisplayName=session.getAttribute("displayName") :212-218(:216). 조회 시 users JOIN 없이 nickname 그대로 노출(`nickname AS authorName`) — GameCommentsMapper.java:36, GameCommentController.java:179. [확인됨]
- **game_comments INSERT 컬럼 = 정확히 4개**: `(game_id, user_id, nickname, content)` — GameCommentsMapper.java:46-60. user_id nullable FK(schema.sql:112), nickname varchar(100) nullable(:102). [확인됨, 비권위 schema]
- 유니크 제약 없음 → 같은 (game,user) 댓글 여러 개 가능. 화면 작성자명 보이려면 nickname 직접 채워야(조회가 users JOIN 안 함). 목록 정렬 created_at ASC, id ASC — GameCommentsMapper.java:42. [확인됨]
- **CSRF**: Spring Security 미사용 → 표준 CSRF 필터 없음. 대신 자체 `CsrfTokens` 유틸을 컨트롤러 mutation 진입부에서 수동검증. [확인됨]
- 리뷰 POST/PUT/DELETE: GameReviewController.java:83-85,134-136,178-180. 댓글: GameCommentController.java:64-66,111-113,151-153. GET 은 검증 없음. [확인됨]
- 토큰: 세션 key `"csrfToken"`, 헤더 `X-CSRF-Token`(우선) 또는 폼 파라미터 `_csrf`(폴백), 32byte SecureRandom Base64url — CsrfTokens.java:12-13,20-33,35-49. [확인됨]
- **영향**: HTTP API 경유 수동 작성은 로그인세션+csrfToken+헤더/파라미터 필요. **DB 직접 INSERT seed 는 CSRF·세션·중복선검사 모두 우회**(단 DB 유니크idx·CHECK·FK 는 그대로 적용). [확인됨]
- 신뢰도: 확인됨. schema.sql/ddl 유래 제약만 추정.
### 포인트 C: seed 적용 경로 + 게임 표시 조건
- 경로: `.env`, `docker-compose.yml`, `src/main/resources/dev/db.properties`, `GameController.java`, `GamesMapper.java`,
`data/GameData.java`, `db/schema.sql`, `WEB-INF/views/game-detail.jsp`
요약:
- **DB 접속 정보 확정** (.env + docker-compose.yml + dev/db.properties 일관) [확인됨, 실행 확인]:
- DB명 `bibimbap`, 유저 `bibimbap`, 패스워드 `change_me_local_dev`, 호스트포트 5433 → 컨테이너 5432.
- .env: POSTGRES_DB/USER=bibimbap, POSTGRES_PASSWORD=change_me_local_dev, APP_SCHEMA=dev, DB_PORT=5433
- docker-compose.yml:18 `"${DB_PORT:-5432}:5432"`, :40 `jdbc:...:5432/${POSTGRES_DB}?currentSchema=${APP_SCHEMA:-dev}`
- dev/db.properties:2-4 `jdbc:postgresql://localhost:5433/bibimbap?currentSchema=dev`
- 컨테이너 `bibimbap-db postgres:16 Up (healthy) 0.0.0.0:5433->5432/tcp` (docker compose ps 실행 확인)
- **앱 스키마 = `dev`**. psql 접속: `PGPASSWORD=change_me_local_dev psql -h localhost -p 5433 -U bibimbap -d bibimbap``SET search_path TO dev;` (또는 `dev.games` 한정).
- **현재 dev.games / dev.users 모두 0행** (read-only 조회 확인). live 스키마는 테이블 없음 → 앱 데이터는 전적으로 dev. [확인됨]
- **게임 표시 조건** (목록과 상세가 다름; games-users INNER JOIN `JOIN users u ON u.id=g.user_id` → 유효 미삭제 user 필수) — GamesMapper.java:32-33. [확인됨]
- 상세(getGame, GamesMapper.java:34-36): `g.id=#{id} AND g.is_delete IS NOT TRUE AND u.is_delete IS NOT TRUE`. **is_visible 조건 없음** → 비공개 게임도 id 직접 접근 시 상세 뜸. [확인됨]
- 목록(getVisibleGames, GamesMapper.java:57-59): `g.is_visible IS NOT FALSE AND g.is_delete IS NOT TRUE AND u.is_delete IS NOT TRUE`. [확인됨]
- **목록+상세 모두 보이려면**: games.is_visible=true, games.is_delete=false, 연결 users.is_delete=false. [확인됨]
- 컨트롤러: gameDetail 이 getGame(id)!=null 이면 DB 게임으로 game-detail 렌더 — GameController.java:113-121. [확인됨]
- **games INSERT 필수(NOT NULL, no-default) 컬럼 = `user_id`(FK→users), `name` 둘뿐** — schema.sql:75-87. [확인됨, 비권위 schema]
- default 보유(생략가능): like_count(0), is_visible(true), sort_order(0), created_at/updated_at(now()), is_delete(false). nullable: creator_note, git_url, webgl_path, thumbnail_url. id=seq.
- 최소 INSERT `(user_id, name)` 만으로 가능 + 목록노출 조건 자동충족. 단 user_id FK 로 **users 행 선행 필수**.
- **asset 컬럼 NULL 이어도 상세/리뷰 동작 — 깨지지 않음** [확인됨]:
- game-detail.jsp:12-21 webglFrameSrc null/blank/"null" 시 빈 문자열 폴백 → iframe src="" 빈화면(예외 없음). 컨트롤러가 trimToEmpty 정규화 후 모델 주입(GameController.java:273,281-283)이라 NPE 없음.
- thumbnail_url 은 상세 JSP 미사용. 리뷰/댓글은 별도 비동기 API(`/game/{id}/reviews`,`/comments`)로 로드 → game asset 컬럼과 무관.
- 결론: webgl_path/thumbnail_url 을 NULL 로 둬도 상세 + 리뷰/댓글 테스트 가능.
- 신뢰도: 확인됨 (코드 + 실행). schema.sql 유래 제약만 추정.
---
## 종합 판단 (더미 seed 실행 청사진)
세 포인트가 한 흐름으로 맞물린다. **현실적 seed 경로 = dev 스키마에 psql 직접 INSERT** (앱 UI 경유는 CSRF+세션+WebGL 업로드 강제로 고비용; DB 직삽은 그 전부 우회하되 FK·CHECK·유니크idx 만 적용).
INSERT 순서(FK 의존성):
1. **users** 1행: status='ACTIVE'(필수 — 아니면 로그인 거부), role='USER', is_delete=false, display_name 권장(목록 creator + 댓글 nickname 소스).
2. **user_auth_identities** 1행: user_id=위 user, provider='email', provider_user_id=<정규화 소문자 이메일>, password_hash=**PBKDF2 형식** `pbkdf2_sha256$210000$<b64salt>$<b64hash>`, is_delete=false. (활성 유니크 idx 주의)
3. **games** 1행: user_id=위 user, name=값. is_visible/is_delete 는 default 로 목록노출 충족. webgl_path/thumbnail_url NULL 가능.
4. **game_reviews** N행: (game_id, user_id, rating 1~5, body). **한 게임당 user 1활성리뷰** → 여러 리뷰면 user 여러 개 seed.
5. **game_comments** N행: (game_id, user_id, nickname[직접 채움], content). 유니크 없음 → 자유 다수.
**로그인 가능 핵심(질문의 최우선 관심사)**: password_hash 는 BCrypt 가 아니라 PBKDF2WithHmacSHA256.
평문→해시는 외부 BCrypt 도구로 만들 수 없고, iterations=210000 / 256bit / 16byte salt / base64 스펙으로
`pbkdf2_sha256$210000$<base64salt>$<base64hash>` 문자열을 생성해 넣어야 검증 통과한다.
(해시 생성 스크립트 작성은 design/구현 단계 몫 — research 범위 밖.)
권위 격상 전 검증 필요 항목: db/schema.sql 이 비권위 복원본이므로, 위 컬럼 *제약/타입/default* 는 seed SQL 확정 직전
실 DB `\d dev.users / \d dev.user_auth_identities / \d dev.games / \d dev.game_reviews / \d dev.game_comments` 로 대조 권장.
(컬럼명·매퍼 일치·인덱스 존재는 코드/실행으로 확인됨 — 불일치 위험은 타입/제약값에 한정.)
## 미해결
- PBKDF2 seed 해시를 실제로 생성하는 도구/스크립트는 미작성(설계·구현 영역). 본 조사는 "어떤 형식이어야 검증 통과하는가"까지만.
- db/schema.sql 비권위로 인한 컬럼 *제약/타입/default* 의 운영 DB 대조는 미수행(읽기조회로 행수·테이블존재만 확인). seed SQL 확정 전 `\d` 대조 필요.
- game_likes 서버측 like_count 증감 경로(좋아요는 localStorage 클라이언트 토글로 보임) — 더미 리뷰/계정/게임 범위 밖이라 미조사.

View File

@ -0,0 +1,32 @@
# bibimbap 인증: 자체 PBKDF2 해시 (BCrypt 아님)
Spring Security 미사용. 로그인은 `UserController` 자체 구현 password 검증.
password_hash 포맷: `pbkdf2_sha256$<iter>$<base64(salt)>$<base64(hash)>` ($ 4필드)
- 알고리즘 `PBKDF2WithHmacSHA256`, iterations=210000, keyLength=256bit(=32byte), salt=16byte SecureRandom
- base64 = 표준(padding 포함). 검증: `UserController.verifyPassword`
(`src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:46-48, 550-584`).
parts[0]=="pbkdf2_sha256" && len==4 확인 → 동일 iter/salt 재계산 → `MessageDigest.isEqual` 상수시간 비교.
## 함정 (Why save)
외부 BCrypt 생성기로 만든 해시는 **무효** — 형식 자체가 다름. 코드 안 보면 "BCrypt 일 것"이라는
기본 가정이 강해, 로그인 가능 더미/테스트/관리자 계정 seed 마다 반복적으로 부딪힌다.
## 로그인 가능 계정 seed 시 필수 조건
- `users.status = 'ACTIVE'` (UserController.java:148-149, 미충족 시 로그인 거부)
- `user_auth_identities.provider = 'email'` (단일 provider, UserController.java:42)
- `provider_user_id` = 정규화(trim + lowercase) 이메일 (UserController.java:527-533)
- 양쪽 `is_delete = false`
- 세션 귀속 키: 로그인 성공 시 session attribute `userId`(Long) 저장 → 리뷰/댓글 작성 가드가 이 키를 읽음.
## 평문→해시 생성 (Python, Java 와 byte-identical)
```python
import hashlib, base64, os
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac('sha256', pw.encode(), salt, 210000, dklen=32)
h = f"pbkdf2_sha256$210000${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}"
```
로그인 동작 보장은 실제 `verifyPassword` 코드 경로로 positive/negative 둘 다 확인할 것
(호스트 JDK 없으면 `mem:java-crypto-verify-via-jdk-container-when-no-host-jdk` 참조).
근거 세션: .atp/work-session/20260618-121419 (더미 seed). 관련 산출물 `db/seed-dev.sql`.

View File

@ -0,0 +1,27 @@
# bibimbap dev 더미 seed 청사진
현실적 경로 = **dev 스키마에 psql 직접 INSERT**. UI 경유는 CSRF + 세션 + (게임) WebGL 업로드를
강제해 고비용. DB 직삽은 CSRF/세션/중복선검사를 우회하되 FK·CHECK·UNIQUE idx 는 그대로 적용됨.
기성 산출물: `db/seed-dev.sql` (멱등, NOT EXISTS 가드) + `db/seed-dev-teardown.sql` (정리).
적용: `docker exec -i bibimbap-db psql -U bibimbap -d bibimbap -v ON_ERROR_STOP=1 < db/seed-dev.sql`
DB: 가동 컨테이너 `bibimbap-db` (postgres:16, 호스트 5433), db/user `bibimbap`, schema `dev`.
## INSERT 순서 (FK 의존)
1. `users` (status='ACTIVE', role='USER', is_delete=false)
2. `user_auth_identities` (PBKDF2 password_hash — `mem:bibimbap-auth-pbkdf2-not-bcrypt`; provider='email')
3. `games` (user_id FK, name NOT NULL, is_visible=true, is_delete=false → 목록/상세 표시; sort_order=MAX+1)
4. `game_reviews` (game_id, user_id, rating 1~5 CHECK, body)
5. `game_comments` (game_id, user_id nullable, nickname, content)
## 핵심 제약 (코드 교차확인 필요 — schema.sql 은 비권위 복원본)
- **`game_reviews` 부분 UNIQUE `(game_id, user_id) WHERE is_delete IS NOT TRUE`**
→ 한 게임에 더미 리뷰 N개 = **더미 유저 N명** 필요. 한 유저로 N리뷰 불가.
- `game_reviews`/`games` 조회는 `users` JOIN → 작성자명은 `users.display_name` 에서 옴.
- `game_comments` 조회는 users JOIN 안 함 → **작성자명은 `nickname` 컬럼을 직접 채워야** 표시됨
(`GameCommentsMapper.listGameComments`).
- 게임 `webgl_path`/`thumbnail_url` NULL 안전 — `game-detail.jsp:13-21` 가 빈 src 로 처리.
- "리뷰 작성 테스트" 하려면 로그인 계정은 해당 게임에 리뷰 0건이어야 함
(`getActiveReviewByGameAndUser` 중복검사, GameReviewController.java:103-105).
근거 세션: .atp/work-session/20260618-121419.

View File

@ -0,0 +1,25 @@
# 호스트 JDK 부재 시 Java 암호 로직 교차검증 패턴
bibimbap 호스트엔 JDK 없음(빌드/실행은 Docker 경로). 그래서 생성한 password_hash 등이
**실제 Java 코드 경로로 검증되는지**를 추정으로 닫지 말고 컨테이너로 실행 증거를 만든다.
## 패턴
1. 검증 대상 로직을 단일 .java 로 복제(예: `UserController.verifyPassword` 그대로).
2. JRE-only 이미지(`bibimbap-app:latest`)는 `java File.java` 소스모드 불가
(`jdk.compiler` 모듈 없음 → `InternalError: Module jdk.compiler not in boot Layer`).
3. JDK 이미지로 컴파일+실행:
```
docker run --rm -v /tmp/HashVerify.java:/tmp/HashVerify.java:ro \
--entrypoint sh eclipse-temurin:21-jdk \
-c 'javac -d /out /tmp/HashVerify.java && java -cp /out HashVerify'
```
4. **positive + negative 둘 다** 확인(올바른 비번=true, 틀린 비번=false). 한쪽만 true 는 불충분.
## 왜
- self-report("스펙상 맞을 것") 대신 실행 증거로 AC 를 닫음.
- Python `hashlib.pbkdf2_hmac('sha256',...)` 와 Java `PBKDF2WithHmacSHA256` 는 byte-identical 이지만,
"동일하다"는 가정 자체를 실제 Java 경로 실행으로 검증.
- 호스트 `timeout` 명령은 macOS 기본 미존재(gtimeout). `/usr/bin/java` 는 JRE 없는 스텁일 수 있음.
일반화: 호스트에 런타임 없을 때 "추정으로 닫기" 대신 throwaway 컨테이너로 실행 증거 확보.
근거 세션: .atp/work-session/20260618-121419.

View File

@ -1,7 +1,7 @@
# Task Completion — bibimbap
코딩 작업 완료 시:
1. `./mvnw test` 통과 (JUnit). `BibimbapApplicationTests` 는 컨텍스트 로드 — 유효 datasource(또는 스킵 조건) 필요.
1. `./mvnw test` 통과 (JUnit). `BibimbapApplicationTests` 는 컨텍스트 로드 — 유효 datasource(또는 스킵 조건) 필요. **MyBatis/DataSource autoconfigure 가 exclude 돼 있어 @Mapper 빈이 자동생성되지 않으므로, 컨트롤러가 주입하는 매퍼마다 `@MockBean` 을 수동 등록해야 한다.** 신규 컨트롤러를 추가하거나 컨트롤러의 매퍼 의존을 늘리면 `BibimbapApplicationTests` 에 해당 매퍼 `@MockBean` 을 추가하지 않는 한 `contextLoads``NoSuchBeanDefinitionException` 으로 실패한다. `test-compile` 만으로는 못 잡으므로 반드시 full `./mvnw test` 로 확인 (W3-2 세션 20260618 회귀 실증).
2. `./mvnw clean package` 로 WAR 빌드 확인 — JSP/컴파일 오류는 단위테스트가 못 잡으므로 패키지까지 돌려 확인.
3. 린터/포매터 없음 (spotless/checkstyle 미설정) — 실행할 것 없음.
4. DB 스키마 변경 시: `src/test/db/dev-to-live-update.sql` + `DbUpdateQueryGeneratorTest` 패턴 참조 (dev→live 마이그레이션 쿼리 생성).

39
db/seed-dev-teardown.sql Normal file
View File

@ -0,0 +1,39 @@
-- =============================================================================
-- bibimbap dev seed teardown — seed-dev.sql 로 만든 더미 데이터만 정리
-- =============================================================================
-- seed-dev.sql 이 생성한 더미(@bibimbap.local 유저 + '테스트 게임 (더미)' + 그
-- 게임의 리뷰/댓글)만 hard delete 한다. 운영성 데이터는 건드리지 않는다.
--
-- 적용:
-- docker exec -i bibimbap-db psql -U bibimbap -d bibimbap -v ON_ERROR_STOP=1 < db/seed-dev-teardown.sql
-- PGPASSWORD=change_me_local_dev psql -h localhost -p 5433 -U bibimbap -d bibimbap -f db/seed-dev-teardown.sql
--
-- FK 의존 역순 삭제: reviews/comments/likes → games → auth_identities → users.
-- =============================================================================
SET search_path TO dev;
DO $$
DECLARE
v_game_id bigint;
BEGIN
SELECT id INTO v_game_id FROM games WHERE name = '테스트 게임 (더미)' LIMIT 1;
IF v_game_id IS NOT NULL THEN
DELETE FROM game_reviews WHERE game_id = v_game_id;
DELETE FROM game_comments WHERE game_id = v_game_id;
DELETE FROM game_likes WHERE game_id = v_game_id;
DELETE FROM games WHERE id = v_game_id;
END IF;
DELETE FROM user_auth_identities
WHERE user_id IN (SELECT id FROM users WHERE canonical_email LIKE '%@bibimbap.local');
DELETE FROM users WHERE canonical_email LIKE '%@bibimbap.local';
RAISE NOTICE 'seed-dev teardown 완료 (game_id=%)', v_game_id;
END $$;
SELECT
(SELECT count(*) FROM users WHERE canonical_email LIKE '%@bibimbap.local') AS remaining_dummy_users,
(SELECT count(*) FROM games WHERE name = '테스트 게임 (더미)') AS remaining_dummy_games;

159
db/seed-dev.sql Normal file
View File

@ -0,0 +1,159 @@
-- =============================================================================
-- bibimbap dev seed — 리뷰/댓글(W3-2) 기능 테스트용 더미 상황
-- =============================================================================
-- 목적: 새로 추가된 게임 리뷰/댓글 기능을 실제로 테스트할 수 있는 더미 데이터.
-- 1) 로그인 가능한 테스트 계정 1개 (직접 로그인 → 리뷰/댓글 작성 테스트용)
-- 2) 표시되는 더미 게임 1개 (테스트 계정 소유)
-- 3) 이미 등록된 더미 리뷰 5개 (각자 다른 더미 유저 — game당 user 1리뷰 제약 때문)
-- + 보너스: 더미 댓글 3개
--
-- 멱등(idempotent): 같은 이메일/게임명 기준 NOT EXISTS 가드 → 재실행해도 중복 미생성.
--
-- 적용 (가동중인 컨테이너):
-- docker exec -i bibimbap-db psql -U bibimbap -d bibimbap -v ON_ERROR_STOP=1 < db/seed-dev.sql
-- 적용 (호스트 psql, 포트 5433):
-- PGPASSWORD=change_me_local_dev psql -h localhost -p 5433 -U bibimbap -d bibimbap -f db/seed-dev.sql
--
-- 로그인 자격: tester@bibimbap.local / test1234! (provider='email')
-- password_hash 는 UserController.hashPassword 와 동일 알고리즘으로 사전 생성:
-- PBKDF2WithHmacSHA256 / 210000 iter / 256-bit key / 16-byte salt,
-- 포맷 = pbkdf2_sha256$<iter>$<base64 salt>$<base64 hash>.
--
-- 되돌리기 (이 더미만 정리): db/seed-dev-teardown.sql 참고.
-- =============================================================================
SET search_path TO dev;
DO $$
DECLARE
v_tester_id bigint;
v_game_id bigint;
v_uid bigint;
-- test1234! 의 사전 생성 해시 (위 알고리즘과 동일).
c_pw_hash text := 'pbkdf2_sha256$210000$QJ5MV+dx3aECELuuh3ibIA==$/59CO6JWXXGXWW0hzqpnZwu89qirDozP0jlq3JzEwZw=';
r RECORD;
BEGIN
-- -------------------------------------------------------------------------
-- 1) 로그인 테스트 계정 (users + email identity)
-- -------------------------------------------------------------------------
SELECT id INTO v_tester_id
FROM users
WHERE canonical_email = 'tester@bibimbap.local' AND is_delete IS NOT TRUE
LIMIT 1;
IF v_tester_id IS NULL THEN
INSERT INTO users (display_name, canonical_email, role, status)
VALUES ('테스터', 'tester@bibimbap.local', 'USER', 'ACTIVE')
RETURNING id INTO v_tester_id;
END IF;
IF NOT EXISTS (
SELECT 1 FROM user_auth_identities
WHERE provider = 'email' AND provider_user_id = 'tester@bibimbap.local'
AND is_delete IS NOT TRUE
) THEN
INSERT INTO user_auth_identities
(user_id, provider, provider_user_id, email, password_hash, display_name)
VALUES
(v_tester_id, 'email', 'tester@bibimbap.local', 'tester@bibimbap.local', c_pw_hash, '테스터');
END IF;
-- -------------------------------------------------------------------------
-- 2) 더미 게임 (테스트 계정 소유, 목록/상세에 표시되도록 visible)
-- webgl_path / thumbnail_url 은 NULL — game-detail.jsp 가 빈 src 로 안전 처리.
-- -------------------------------------------------------------------------
SELECT id INTO v_game_id
FROM games
WHERE name = '테스트 게임 (더미)' AND is_delete IS NOT TRUE
LIMIT 1;
IF v_game_id IS NULL THEN
INSERT INTO games (user_id, name, creator_note, is_visible, sort_order)
VALUES (
v_tester_id,
'테스트 게임 (더미)',
'리뷰/댓글 기능 테스트용 더미 게임입니다. 자유롭게 리뷰와 댓글을 남겨보세요.',
true,
(SELECT COALESCE(MAX(sort_order), 0) + 1 FROM games WHERE is_delete IS NOT TRUE)
)
RETURNING id INTO v_game_id;
END IF;
-- -------------------------------------------------------------------------
-- 3) 더미 리뷰어 5명 + 각자 리뷰 1건 (game당 user 1리뷰 unique 제약 충족)
-- created_at 을 days_ago 만큼 과거로 스태거 → 목록(최신순) 정렬이 자연스럽게.
-- -------------------------------------------------------------------------
FOR r IN
SELECT * FROM (VALUES
('김플레이', 'reviewer1@bibimbap.local', 5, '그래픽이 깔끔하고 조작감이 좋아요. 가볍게 즐기기 딱 좋습니다. 추천!', 5),
('이도전', 'reviewer2@bibimbap.local', 4, '아이디어가 신선했습니다. 난이도 밸런스만 조금 더 다듬으면 완벽할 듯해요.', 4),
('박캐주얼', 'reviewer3@bibimbap.local', 3, '무난하게 즐길 만한 게임. 다만 후반부가 살짝 반복적으로 느껴졌어요.', 3),
('최열정', 'reviewer4@bibimbap.local', 5, '시간 가는 줄 모르고 플레이했네요. BGM이 특히 인상적이었습니다!', 2),
('정라이트', 'reviewer5@bibimbap.local', 2, '초반 튜토리얼이 불친절해서 적응이 조금 어려웠어요. 보완되면 좋겠습니다.', 1)
) AS t(nickname, email, rating, body, days_ago)
LOOP
SELECT id INTO v_uid
FROM users
WHERE canonical_email = r.email AND is_delete IS NOT TRUE
LIMIT 1;
IF v_uid IS NULL THEN
INSERT INTO users (display_name, canonical_email, role, status)
VALUES (r.nickname, r.email, 'USER', 'ACTIVE')
RETURNING id INTO v_uid;
END IF;
IF NOT EXISTS (
SELECT 1 FROM game_reviews
WHERE game_id = v_game_id AND user_id = v_uid AND is_delete IS NOT TRUE
) THEN
INSERT INTO game_reviews (game_id, user_id, rating, body, created_at, updated_at)
VALUES (
v_game_id, v_uid, r.rating, r.body,
now() - make_interval(days => r.days_ago),
now() - make_interval(days => r.days_ago)
);
END IF;
END LOOP;
-- -------------------------------------------------------------------------
-- 보너스) 더미 댓글 3건 (리뷰어 유저 귀속, created_at ASC 정렬용 스태거)
-- -------------------------------------------------------------------------
FOR r IN
SELECT * FROM (VALUES
('reviewer1@bibimbap.local', '오 이거 재밌네요 ㅎㅎ', 3),
('reviewer3@bibimbap.local', '다음 업데이트도 기대할게요!', 2),
('reviewer5@bibimbap.local', '버그 제보: 가끔 화면이 멈춰요.', 1)
) AS t(email, content, days_ago)
LOOP
SELECT id INTO v_uid
FROM users
WHERE canonical_email = r.email AND is_delete IS NOT TRUE
LIMIT 1;
IF v_uid IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM game_comments
WHERE game_id = v_game_id AND user_id = v_uid AND content = r.content
AND is_delete IS NOT TRUE
) THEN
INSERT INTO game_comments (game_id, user_id, nickname, content, created_at)
VALUES (
v_game_id, v_uid,
(SELECT display_name FROM users WHERE id = v_uid),
r.content,
now() - make_interval(days => r.days_ago)
);
END IF;
END LOOP;
RAISE NOTICE 'seed-dev 완료: tester_id=%, game_id=%', v_tester_id, v_game_id;
END $$;
-- 적용 결과 확인용 요약 (psql 실행 시 출력).
SELECT
(SELECT count(*) FROM users WHERE canonical_email LIKE '%@bibimbap.local' AND is_delete IS NOT TRUE) AS dummy_users,
(SELECT count(*) FROM games WHERE name = '테스트 게임 (더미)' AND is_delete IS NOT TRUE) AS dummy_games,
(SELECT count(*) FROM game_reviews r JOIN games g ON g.id = r.game_id
WHERE g.name = '테스트 게임 (더미)' AND r.is_delete IS NOT TRUE) AS dummy_reviews,
(SELECT count(*) FROM game_comments c JOIN games g ON g.id = c.game_id
WHERE g.name = '테스트 게임 (더미)' AND c.is_delete IS NOT TRUE) AS dummy_comments;