Skip to content

[Bug] sirsoft-board 댓글·대댓글 깊이 및 무결성 버그 보고 #80

Description

@jiwonpapa

sirsoft-board 댓글·대댓글 깊이 및 무결성 버그 보고

요약

sirsoft-board의 댓글·대댓글 처리에서 다음 세 가지 문제가 확인됩니다.

구분 문제 영향
대댓글 깊이 max_comment_depth는 최대 10이지만 저장 depth가 5로 고정됨 6단계 이후 깊이 제한 무력화 및 표시 계층 왜곡
부모 댓글 무결성 parent_id가 같은 게시글의 댓글인지 검증하지 않음 다른 게시글 댓글을 부모로 가진 고아 댓글 생성 가능
REST 경로 무결성 댓글 수정·삭제에서 URL의 postId를 사용하지 않음 다른 게시글 경로로 댓글 수정·삭제 가능

가장 우선도가 높은 문제는 첫 번째입니다. 기본 max_comment_depth가 10이므로 별도 설정을 변경하지 않은 게시판도 영향을 받습니다.

확인 환경

항목
그누보드 버전 7.0.4
게시판 모듈 sirsoft-board 1.2.0
확인 커밋 348efa2e98df22b00d30d3a4e267d0305f201758
확인일 2026-07-17

관련 원본 파일에 로컬 변경이 없는 상태에서 분석했습니다.


이슈 1. max_comment_depth 6~10에서 depth가 5로 고정되고 제한이 무력화됨

제안 제목

[sirsoft-board] max_comment_depth 6~10 설정에서 depth가 5로 고정되고 대댓글 제한이 무력화됨

문제 정의

게시판 설정과 DB 스키마는 대댓글 깊이를 최대 10까지 지원하지만, 댓글 생성 서비스는 계산된 depth를 숫자 5로 강제 제한합니다.

설정 기본값:

"max_comment_depth": 10

근거: modules/_bundled/sirsoft-board/config/settings/defaults.json:18

설정 허용 범위:

'max_comment_depth_min' => 0,
'max_comment_depth_max' => 10,

근거: modules/_bundled/sirsoft-board/config/board.php:150-152

DB 기본값:

$table->unsignedSmallInteger('max_comment_depth')
    ->default(10)
    ->comment('대댓글 최대 깊이 (1~10)');

근거: modules/_bundled/sirsoft-board/database/migrations/2026_04_01_000002_create_boards_table.php:46

실제 댓글 생성 코드:

// 부모 댓글의 depth + 1 (최대 5까지)
$data['depth'] = min(($parentComment->depth ?? 0) + 1, 5);

근거: modules/_bundled/sirsoft-board/src/Services/CommentService.php:252-257

깊이 검증 코드:

if ($parentComment->depth + 1 > $board->max_comment_depth) {
    $fail(__('sirsoft-board::validation.comment.depth.exceeded', [
        'max' => $board->max_comment_depth,
    ]));
}

근거: modules/_bundled/sirsoft-board/src/Rules/CommentValidationRule.php:113-116

재현 절차

  1. 대댓글 사용이 활성화된 게시판의 max_comment_depth를 10으로 설정합니다.
  2. 게시글에 최상위 댓글을 작성합니다.
  3. 직전에 작성한 댓글 ID를 다음 요청의 parent_id로 지정합니다.
  4. 동일한 방식으로 11단계 이상 대댓글을 연속 작성합니다.
  5. 댓글 생성 응답 또는 댓글 목록 API에서 각 댓글의 depth를 확인합니다.

사용자 댓글 생성 API:

POST /api/modules/sirsoft-board/boards/{slug}/posts/{postId}/comments
Content-Type: application/json

{
  "content": "대댓글 깊이 확인",
  "parent_id": 123
}

라우트 근거: modules/_bundled/sirsoft-board/src/routes/api.php:479-488

예상 결과

대댓글 단계 저장 depth 응답
1~10 1~10 201 Created
11 저장하지 않음 422 Unprocessable Entity

실제 결과

현재 코드 수식을 그대로 적용하면 다음과 같이 처리됩니다.

대댓글 단계 부모 depth 검증 저장 depth
1 0 통과 1
2 1 통과 2
3 2 통과 3
4 3 통과 4
5 4 통과 5
6 5 통과 5
7 이상 5 계속 통과 계속 5

max_comment_depth=10일 때 부모 depth가 5이면 검증식은 항상 5 + 1 > 10이 되어 실패하지 않습니다. 생성 서비스는 다시 min(6, 5)를 저장하므로 이후 부모 댓글도 계속 depth=5입니다.

사용자 화면 영향

사용자 템플릿은 API의 depth만큼 들여쓰기하고, 답글 버튼도 depth < max_comment_depth 조건으로 표시합니다.

"marginLeft": "{{Math.min(comment?.depth ?? 0, 10) * 1}}rem"
"if": "{{comment?.abilities?.can_write && (comment?.depth ?? 0) < (post?.data?.board?.max_comment_depth ?? 10) ...}}"

근거: templates/_bundled/sirsoft-basic/layouts/partials/board/show/_comment_item.json:11, :121

따라서 6단계 이후 댓글은 계속 5단계로 표시되면서 답글 버튼은 계속 노출됩니다.

성능 영향

댓글 목록은 부모·자식 정렬과 자손 댓글 수 계산을 재귀적으로 수행합니다.

근거:

  • modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php:160-195
  • modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php:206-235

깊이 제한이 무력화된 긴 단일 체인에서는 재귀 깊이가 증가합니다. 또한 모든 댓글에서 다시 전체 자손을 계산하므로 긴 체인일수록 CPU 비용이 커질 수 있습니다.

테스트 누락

현재 경계 테스트는 max_comment_depth=2만 사용합니다. 서비스에 하드코딩된 5를 넘지 않기 때문에 이 문제를 검출하지 못합니다.

근거: modules/_bundled/sirsoft-board/tests/Feature/User/CommentCrudScenarioTest.php:189-205

수정 방향

$data['depth'] = ($parentComment->depth ?? 0) + 1;

생성 서비스의 숫자 5 하드코딩을 제거하고, 게시판별 최대 깊이 초과는 기존 CommentValidationRule에서 거부하는 방식이 적절합니다. 저장 단계에서도 방어적으로 계산된 깊이가 게시판 설정을 초과하지 않는지 확인할 수 있습니다.

완료 조건

  • max_comment_depth=10에서 depth=1~10이 정확히 저장됩니다.
  • depth=10 댓글에 답글을 작성하면 422를 반환합니다.
  • max_comment_depth=0, 1, 5, 6, 10 경계 테스트가 추가됩니다.
  • 저장된 depth와 실제 parent_id 체인의 깊이가 일치합니다.
  • 사용자 및 관리자 댓글 생성 API에 동일한 규칙이 적용됩니다.

이슈 2. 다른 게시글 댓글을 parent_id로 지정할 수 있음

제안 제목

[sirsoft-board] 대댓글 parent_id가 동일 게시글 소속인지 검증하지 않아 고아 댓글이 생성됨

문제 정의

대댓글 생성 요청은 URL의 postIdpost_id로 사용하지만, parent_id 검증은 부모 댓글의 board_id만 확인합니다. 부모 댓글의 post_id가 현재 게시글과 같은지 확인하지 않습니다.

요청 검증:

'post_id' => ['bail', 'required', 'integer', new CommentValidationRule($slug, 'post')],
'parent_id' => [
    'nullable',
    'integer',
    new CommentValidationRule($slug, 'parent_comment'),
],

근거: modules/_bundled/sirsoft-board/src/Http/Requests/StoreCommentRequest.php:75-80

부모 댓글 조회:

$parentComment = $board
    ? Comment::where('board_id', $board->id)->withTrashed()->find($parentId)
    : null;

근거: modules/_bundled/sirsoft-board/src/Rules/CommentValidationRule.php:88-93

Repository 조회도 게시판과 댓글 ID만 사용합니다.

근거: modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php:259-267

재현 절차

  1. 동일 게시판에 게시글 A와 게시글 B를 생성합니다.
  2. 게시글 A에 댓글 A-1을 작성합니다.
  3. 게시글 B의 댓글 생성 API에 댓글 A-1의 ID를 parent_id로 전달합니다.
POST /api/modules/sirsoft-board/boards/{slug}/posts/{postBId}/comments
Content-Type: application/json

{
  "content": "게시글 B에 작성하지만 부모는 게시글 A의 댓글",
  "parent_id": "{commentA1Id}"
}

예상 결과

부모 댓글이 현재 게시글 소속이 아니므로 422 Unprocessable Entity를 반환해야 합니다.

현재 코드 경로

같은 게시판의 유효한 댓글이므로 부모 댓글 검증을 통과합니다. 생성되는 행은 post_id=게시글 B, parent_id=게시글 A의 댓글 조합을 가질 수 있습니다.

댓글 목록 조회는 현재 게시글의 post_id로만 댓글을 가져오고 parent_id=null인 댓글부터 트리를 구성합니다.

근거:

  • modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php:45-48
  • modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php:160-175

따라서 게시글 B에 저장된 자식 댓글은 부모가 조회 결과에 없어 화면에서 누락될 수 있습니다.

추가 영향

  • 부모 댓글의 replies_count가 다른 게시글의 대댓글로 증가할 수 있습니다.
  • 게시글 B의 정보와 게시글 A 댓글 작성자를 조합한 잘못된 답글 알림이 발송될 수 있습니다.
  • DB는 파티션 구조 때문에 parent_id 외래키가 없으므로 애플리케이션 검증 누락이 그대로 데이터 무결성 훼손으로 이어집니다.

근거:

  • modules/_bundled/sirsoft-board/src/Listeners/CommentReplySyncListener.php:47-54
  • modules/_bundled/sirsoft-board/src/Listeners/BoardNotificationDataListener.php:181-206
  • modules/_bundled/sirsoft-board/database/migrations/2026_04_01_000005_create_board_comments_table.php:28

테스트 누락

현재 테스트는 존재하지 않는 parent_id만 확인합니다. 같은 게시판의 다른 게시글에 존재하는 댓글 ID는 테스트하지 않습니다.

근거: modules/_bundled/sirsoft-board/tests/Feature/User/CommentCrudScenarioTest.php:208-221

완료 조건

  • 부모 댓글의 board_idpost_id가 현재 요청 대상과 모두 일치해야 합니다.
  • 같은 게시판의 다른 게시글 댓글을 parent_id로 전달하면 422를 반환해야 합니다.
  • 사용자·관리자 댓글 생성 API에 동일한 검증이 적용되어야 합니다.
  • 잘못된 부모 관계에서는 댓글, 답글 수 및 알림 후처리가 실행되지 않아야 합니다.

이슈 3. 댓글 수정·삭제에서 URL postId를 검증하지 않음

제안 제목

[sirsoft-board] 댓글 수정·삭제 API가 URL의 postId와 댓글 소속 post_id 일치를 검증하지 않음

문제 정의

사용자와 관리자 댓글 수정·삭제 컨트롤러는 $postId를 인자로 받지만 댓글 조회 및 수정·삭제 과정에서 사용하지 않습니다.

사용자 API 근거:

  • modules/_bundled/sirsoft-board/src/Http/Controllers/User/CommentController.php:119-149
  • modules/_bundled/sirsoft-board/src/Http/Controllers/User/CommentController.php:165-195

관리자 API 근거:

  • modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/CommentController.php:121-144
  • modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/CommentController.php:160-179

댓글 Repository는 board_id + commentId로만 댓글을 찾습니다.

근거: modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php:279-287

재현 절차

  1. 동일 게시판에 게시글 A와 B를 생성합니다.
  2. 게시글 A에 본인 댓글 A-1을 작성합니다.
  3. 게시글 B의 URL에 댓글 A-1 ID를 넣어 수정 또는 삭제를 요청합니다.
PUT /api/modules/sirsoft-board/boards/{slug}/posts/{postBId}/comments/{commentA1Id}
DELETE /api/modules/sirsoft-board/boards/{slug}/posts/{postBId}/comments/{commentA1Id}

예상 결과

댓글이 URL의 게시글에 속하지 않으므로 404 또는 422를 반환해야 합니다.

현재 코드 경로

댓글 작성자 또는 관리 권한 조건만 충족하면 URL의 postId와 무관하게 대상 댓글을 수정·삭제할 수 있습니다.

완료 조건

  • 댓글 조회 Repository 또는 Service가 board_id + post_id + comment_id로 대상을 제한해야 합니다.
  • 다른 게시글 URL을 사용한 수정·삭제 요청은 실패해야 합니다.
  • 사용자 및 관리자 API 모두 교차 게시글 경로 테스트를 추가해야 합니다.

검증 상태 및 제한

다음 사항은 소스 코드에서 확인했습니다.

  • 기본 대댓글 깊이 10과 서비스의 숫자 5 하드코딩 불일치
  • 5단계 이후 검증식과 저장식이 반복되는 결정적 실행 흐름
  • 부모 댓글 검증의 post_id 조건 누락
  • 댓글 수정·삭제에서 URL의 postId 미사용
  • 관련 경계 테스트 누락

현재 로컬 환경에는 .env.testing 파일이 없어 Laravel HTTP 자동 테스트는 실행하지 못했습니다. 실제 수정 시 위 재현 시나리오를 Feature 테스트로 추가해야 합니다.

권장 처리 순서

  1. 대댓글 depth 숫자 5 하드코딩 제거 및 경계 테스트 추가
  2. 부모 댓글의 동일 게시글 소속 검증 추가
  3. 댓글 수정·삭제의 postId 스코프 검증 추가
  4. 잘못된 부모 관계로 이미 저장된 데이터 탐지 쿼리 및 정리 절차 제공

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions