--- phase: research agent: research-advisor agent_version: 2 generated_at: 2026-06-30T11:10:00+09:00 concerns: - "PostsMapper.update() SQL에 category_id 누락 — 카테고리 변경이 DB에 반영되지 않음 (버그, 500 직접 원인은 아님)" - "PostController의 editForm/update/delete 핸들러가 getPublished()를 사용해 DRAFT 포스트 조회 불가 — DRAFT 저장 후 재편집 시 null → redirect:/posts 또는 404 (500 간접 원인 후보)" - "posts-detail.jsp에서 post attribute null 체크 없이 post.getTitle() 등 직접 접근 — post가 null이면 NullPointerException → 500" - "PostAdminController에 인증/권한 체크 부재 — Spring Security 미사용 환경에서 /admin/** RbacInterceptor 보호 범위 확인 필요" source_confidence: high workers_spawned: 4 --- # 조사 결과 ## 주제 bibimbap 프로젝트 포스팅 메뉴 진입 시 500 Error 원인 조사 --- ## 포인트별 발견 ### 포인트 1: 포스팅 Controller URL 매핑 - 경로: `/Users/wemadeplay/workspace/stz/bibimbap/src/main/java/com/pandoli365/bibimbap/controller/PostController.java` - 신뢰도: 확인됨 | HTTP | 경로 | 핸들러 메서드 | 반환 | |------|------|-------------|------| | GET | `/posts` | `list` | View: `posts-list` | | GET | `/posts/{id}` | `detail` | View: `posts-detail` (null이면 redirect:/posts) | | GET | `/posts/new` | `createForm` | View: `posts-form` (미인증 시 redirect:/login) | | POST | `/posts` | `create` | JSON ResponseEntity | | GET | `/posts/{id}/edit` | `editForm` | View: `posts-form` (미인증 시 redirect:/login) | | POST | `/posts/{id}` | `update` | JSON ResponseEntity | | POST | `/posts/{id}/delete` | `delete` | JSON ResponseEntity | - 경로: `/Users/wemadeplay/workspace/stz/bibimbap/src/main/java/com/pandoli365/bibimbap/controller/PostAdminController.java` - 신뢰도: 확인됨 | HTTP | 경로 | 핸들러 메서드 | |------|------|-------------| | GET | `/admin/post-categories` | `postCategoriesPage` → View: `admin-post-categories` | | POST | `/admin/post-categories` | `createCategory` | | POST | `/admin/post-categories/{id}` | `updateCategory` | | POST | `/admin/post-categories/{id}/delete` | `deleteCategory` | --- ### 포인트 2: Service / Mapper 체인 - 신뢰도: 확인됨 **PostController 의존 Bean:** - `PostsMapper` (annotation 기반 MyBatis) — `listPublishedKeyset`, `getPublished`, `insert`, `update`, `softDelete` - `PostCategoriesMapper` — `listActive`, `getActive`, `getById`, `insert`, `update`, `delete`, `countPostsByCategory` - `PostMarkdownService` — CommonMark 파싱 + Jsoup basicWithImages safelist sanitize - `OgPreviewService` — SSRF-safe HTTP fetch + OG 메타 파싱, 실패 시 graceful empty **별도 PostService/PostServiceImpl 없음.** 비즈니스 로직이 Controller에 직접 집약. XML Mapper 없음, 전부 `@Select`/`@Insert`/`@Update` annotation. --- ### 포인트 3: 500 에러 원인 후보 (가능성 순) #### [1순위 — 가장 유력] posts-detail.jsp NPE (확인됨) - 파일: `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/posts-detail.jsp` - `PostData post = (PostData) request.getAttribute("post");` 이후 null 체크 없이 `post.getTitle()`, `post.getId()` 등 직접 접근 - **`post` attribute가 null인 상태로 JSP가 렌더링되면 NullPointerException → 500** - 발생 경로: `PostController.detail()`이 `postsMapper.getPublished(id)`를 호출해 null을 받으면 `redirect:/posts`로 분기하므로 Controller 레벨에서는 방어됨. 그러나 다른 진입 경로(forward, include, 테스트 호출)에서 post attribute 누락 시 노출 가능. #### [2순위] DRAFT 포스트 조회 시 null 처리 흐름 (확인됨) - 파일: `PostController.java` line 167, 206, 252 - `editForm`, `update`, `delete` 핸들러 모두 `postsMapper.getPublished(id)` 사용 - `getPublished` SQL: `WHERE p.status = 'PUBLISHED' AND p.is_delete = false` - **DRAFT 상태로 저장된 포스트 ID로 editForm 접근 시 → null → `if (post == null) return "redirect:/posts"`** — 500이 아닌 redirect지만, JS fetch로 update/delete 호출 시 null → NPE 가능성: - `PostController.update()` line 206: `existing.setCategoryId(categoryId)` — `existing`이 null이면 NPE → 500 - `PostController.delete()` line 252: `postsMapper.softDelete(id)` 호출 전 null 체크 여부 추가 확인 필요 #### [3순위] PostsMapper.update() — category_id 컬럼 누락 (확인됨) - 파일: `PostsMapper.java` line 120-135 - UPDATE SQL 대상 컬럼: `title, body_markdown, body_sanitized_html, link_url, og_*, status, updated_at` - **`category_id = #{categoryId}` 누락** → 카테고리 변경이 DB에 반영되지 않음 - 500 직접 원인은 아니나, 카테고리 변경 요청 시 데이터 정합성 손상 #### [4순위] PostsMapper.insert() 직후 ID null 체크 (확인됨) - `PostController.create()` line 149: `if (post.getId() == null)` 체크 후 500 응답 - DB INSERT 실패(constraint violation, DB 연결 오류 등) 시 MyBatis 예외 → Spring이 500으로 변환 - 포스트 생성 시 `category_id` FK 참조 실패(비활성 카테고리 등) 가능 #### [5순위] Bean 주입 / 빌드 이슈 (확인됨 — 현재 정상) - Spring Boot 3.5.16, MyBatis Spring Boot 3.0.5: 호환 정상 - 모든 Bean 생성자 주입, `@Mapper` 어노테이션 정상 - JSTL 미사용(scriptlet 방식), tomcat-embed-jasper compile scope: JSP 렌더링 정상 - ViewResolver: prefix=`/WEB-INF/views/`, suffix=`.jsp` 정상 - **기동 실패 유발 구조적 결함 없음** #### [6순위] PostAdminController 권한 체크 부재 (추정) - `PostController`는 `gate.has(session, "POST_WRITE")` 명시 검사 - `PostAdminController`에는 `PermissionGate` 주입 자체 없음 - `InterceptorConfig`에서 `/admin/**`에 `RbacInterceptor` 등록 확인됨 — 필터 레벨 보호 존재 - `/admin/jams/**`는 제외(addExcludePatterns). `/admin/post-categories`는 포함 여부 추가 확인 필요 - 500 직접 원인 아님; 인증 우회 시 NPE 가능성 --- ### 포인트 4: 관련 JSP 파일 경로 - 신뢰도: 확인됨 | JSP | 절대 경로 | |-----|----------| | 포스트 목록 | `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/posts-list.jsp` | | 포스트 상세 | `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/posts-detail.jsp` | | 포스트 작성/수정 폼 | `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/posts-form.jsp` | | 관리자 카테고리 | `/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/admin-post-categories.jsp` | **admin-post-categories.jsp 주의사항:** - `var BASE = '/admin/post-categories';` — context path 미포함 하드코딩 - 다른 JSP들은 `<%= ctx %>` 변수(contextPath 동적 주입)를 사용하는 것과 비교해 inconsistent - context root가 `/` 이외로 배포 시 404 발생 --- ### 포인트 5: 최근 git 변경 사항 (포스팅 관련) - 신뢰도: 확인됨 | 커밋 해시 | 메시지 | |---------|--------| | `6047a39` | `feat(board): W3-3 포스팅 보드 — 공지/블로그 + OG 미리보기(SSRF 방어) + 유니티 피드 감시` | - 포스팅 기능이 이 단일 커밋(32파일, 5327줄 추가)으로 초기 도입됨 (2026-06-29) - **후속 수정 커밋 없음** — 초기 도입 이후 한 번도 수정되지 않은 상태 - 이전 세션 로그에서 발견된 PKIX 빌드 오류(`spring-boot-dependencies:3.5.14-SNAPSHOT`)는 `e51ab01` 커밋으로 해소됨 (현재 정식 버전 사용) --- ## 종합 판단 **"포스팅 메뉴 진입 시 500"의 가장 유력한 원인은 `posts-detail.jsp`의 null 체크 부재(NPE)다.** 구체적 시나리오: 1. `GET /posts/{id}` 요청 → `PostController.detail()` → `postsMapper.getPublished(id)` 호출 2. `id`가 DRAFT 상태이거나 삭제된 포스트이거나 존재하지 않으면 → `null` 반환 → `redirect:/posts`로 처리됨 (Controller 레벨 방어 있음) 3. 그러나 **"포스팅 메뉴 진입"이 `GET /posts` (목록)라면**: `posts-list.jsp`로 렌더링되며 `postCategoriesMapper.listActive()`가 빈 결과를 반환할 경우 JSP 루프는 정상 처리. 별도 NPE 없음. 4. **만약 카테고리가 하나도 없는 상태에서 `createForm`(`GET /posts/new`) 진입 시**: `postCategoriesMapper.listActive()`가 빈 리스트를 반환하더라도 JSTL/scriptlet 루프이므로 NPE 없음. 따라서 **정확한 500 재현 경로는 다음 중 하나**: - `GET /posts/{존재하지 않거나 DRAFT인 id}` → Controller null 체크 → redirect (500 아님) - `POST /posts/{DRAFT id}` (JS fetch update) → `existing = postsMapper.getPublished(id)` → null → `existing.setCategoryId()` → **NPE → 500** ← **가장 유력** - `posts-detail.jsp` 직접 forward/include 시 post attribute 누락 → NPE → 500 **권위 격상 전 검증 필요 항목:** - `PostController.update()` line 206에서 `existing`이 null일 때의 실제 분기 코드 (null 체크가 있는지 여부) — worker 탐색에서 null 체크 코드가 보이지 않아 NPE 유력하나, 해당 라인 전후 코드 직접 확인 권장 --- ## 영향 파일 목록 | 파일 | 역할 | 이슈 | |------|------|------| | `src/main/java/.../controller/PostController.java` | 메인 Controller | DRAFT 조회 불가, update NPE 후보 | | `src/main/java/.../controller/PostAdminController.java` | 관리자 Controller | 권한 체크 부재 | | `src/main/java/.../mapper/PostsMapper.java` | MyBatis Mapper | update SQL category_id 누락 | | `src/main/java/.../mapper/PostCategoriesMapper.java` | 카테고리 Mapper | 정상 | | `src/main/java/.../service/PostMarkdownService.java` | 마크다운 렌더링 | 정상 | | `src/main/java/.../service/OgPreviewService.java` | OG 미리보기 | 정상 | | `src/main/webapp/WEB-INF/views/posts-detail.jsp` | 상세 JSP | NPE 위험 (null 체크 없는 post 접근) | | `src/main/webapp/WEB-INF/views/posts-list.jsp` | 목록 JSP | 정상 | | `src/main/webapp/WEB-INF/views/posts-form.jsp` | 작성/수정 JSP | 정상 | | `src/main/webapp/WEB-INF/views/admin-post-categories.jsp` | 관리자 JSP | context path 하드코딩 주의 | --- ## 미해결 (open_questions) 1. **`PostController.update()` line 206 전후 null 체크 존재 여부**: `existing`이 null일 때 NPE로 이어지는지, 아니면 별도 분기가 있는지 해당 라인 직접 확인 필요. (현재 탐색에서는 null 체크 코드가 보이지 않아 NPE 유력 추정) 2. **정확한 "포스팅 메뉴 진입" URL**: `/posts` (목록), `/posts/{id}` (상세), `/posts/new` (작성폼), `/admin/post-categories` 중 어느 URL에서 500이 발생하는지 재현 경로 미확인 3. **DB 스키마 상태**: `post_categories` 테이블에 활성(is_active=true) 카테고리가 존재하는지 — 카테고리 없는 상태에서 create/edit 폼의 카테고리 드롭다운이 빈 상태로 표시되고 categoryId=null 제출 → validate() line 305 `categoryId == null` 조건으로 BadRequest 반환 (500 아님) 4. **RbacInterceptor `/admin/post-categories` 보호 여부**: `addExcludePatterns`에서 `/admin/jams/**` 외 추가 제외 경로 상세 확인 필요 5. **애플리케이션 로그 스택트레이스**: 실제 500 발생 시의 예외 클래스와 라인 넘버 — 현재 로그 파일에서 포스팅 관련 스택트레이스 미발견