bibimbap/.atp/work-session/20260618-121419/research/index.md

150 lines
15 KiB
Markdown

---
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 클라이언트 토글로 보임) — 더미 리뷰/계정/게임 범위 밖이라 미조사.