bibimbap/.atp/work-session/20260630-105459/research/posting-500-research.md

11 KiB

phase agent agent_version generated_at concerns source_confidence workers_spawned
research research-advisor 2 2026-06-30T11:10:00+09:00
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 보호 범위 확인 필요
high 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
  • PostCategoriesMapperlistActive, 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 권한 체크 부재 (추정)

  • PostControllergate.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 발생 시의 예외 클래스와 라인 넘버 — 현재 로그 파일에서 포스팅 관련 스택트레이스 미발견