스레드
이온디

./layouts/el_d1/

게시판은 거부한다.

레이아웃 만으로 게시판의 데이터를 처리함.


개발 히스토리

2025.11.29 토요일 프론트 화면 구성

2025.12.01 월요일 백엔드 데이터 연결


# el_d1 레이아웃 댓글 시스템 개발 가이드

## 개요

el_d1 레이아웃에서 댓글 등록/수정/삭제를 구현하는 두 가지 방법:

| 방법 | 사용 페이지 | 핵심 기술 |
|------|-------------|-----------|
| **방법 1** | community_view | 게시판 모듈 컨텍스트 + 폼 제출 |
| **방법 2** | homepage_solution_view | REST API (`executeQuery` 직접 사용) |

---

## 방법 1: 게시판 모듈 컨텍스트 사용

### 사용 위치
`layouts/el_d1/assets/pages/community_view.blade.php`

### 원리
Rhymix 게시판 모듈이 페이지 로드 시 자동으로 `$document`, `$grant` 등의 변수를 Context에 주입합니다. 이 변수들을 그대로 사용하여 일반 폼 제출로 댓글을 등록합니다.

### 데이터 흐름
```
1. URL 접근: /community/123
2. 게시판 모듈 실행
3. Context에 $document, $grant, $category_list 자동 설정
4. 레이아웃 로드 → @include('community_view')
5. 폼 제출 → procBoardInsertComment 실행
6. 성공 시 페이지 리다이렉트
```

### 소스코드 예제

#### PHP 부분 (데이터 접근)
```php
{{-- community_view.blade.php --}}
@version(2)

@php
    // $document, $grant는 게시판 모듈이 자동 주입
    // 별도 조회 코드 불필요!

    if ($document) {
        $cat_srl = $document->get('category_srl');
        $extra_vars = $document->getExtraVars();
        $is_answered = $extra_vars['is_answered'] ?? false;
    }
@endphp

@if($document)
    
        

{{ $document->getTitleText() }}

{!! $document->getContent() !!}

작성자: {{ $document->getNickName() }}

조회수: {{ $document->get('readed_count') }}

@endif ``` #### HTML 부분 (댓글 폼) ```html {{-- 댓글 작성 폼 --}} @if($grant->write_comment) {{-- 필수 hidden 필드 --}} document_srl }}" /> csrf_token }}" /> 댓글 등록 @else

댓글을 작성하려면 로그인이 필요합니다.

@endif ``` #### JavaScript 부분 (답글 기능) ```html function replyComment(parentSrl) { const form = document.getElementById('comment-form'); form.querySelector('input[name="parent_srl"]').value = parentSrl; form.querySelector('textarea').placeholder = '답글을 입력하세요...'; form.querySelector('textarea').focus(); } ``` #### 댓글 수정/삭제 (링크 방식) ```html @if($comment->isGranted()) 수정 삭제 @endif ``` --- ## 방법 2: REST API 사용 (executeQuery 직접) ### 사용 위치 - `layouts/el_d1/assets/pages/homepage_solution_view.blade.php` - `modules/api/rest.php` ### 원리 게시판 모듈의 자동 주입을 사용하지 않고, `getModel('document')`로 직접 데이터를 조회합니다. 댓글 CRUD는 REST API를 통해 `executeQuery`로 직접 DB 작업을 수행합니다. ### 왜 이 방법이 필요한가? 레이아웃 페이지에서 AJAX로 `procBoardInsertComment`를 호출하면: ```javascript // 이 코드는 성공 응답이 오지만 실제로 저장되지 않음! exec_json('procBoardInsertComment', params, function(ret) { console.log(ret); // {error: 0, message: 'success'} // 하지만 새로고침하면 댓글 없음! }); ``` **원인**: 게시판 모듈 컨텍스트가 없어서 액션이 무시됨 **해결**: REST API로 `executeQuery` 직접 실행 ### 데이터 흐름 ``` 1. URL 접근: /homepage_solution/123 2. @php에서 getModel('document')로 직접 조회 3. JavaScript에서 REST API 호출 4. rest.php에서 executeQuery로 DB 직접 작업 5. JSON 응답 → 페이지 동적 업데이트 ``` ### 소스코드 예제 #### PHP 부분 (직접 데이터 조회) ```php {{-- homepage_solution_view.blade.php --}} @version(2) @php $context_document_srl = Context::get('document_srl'); $doc = null; $comments = []; $api_grant = null; if ($context_document_srl) { // 문서 조회 $oDocumentModel = getModel('document'); $oDocument = $oDocumentModel->getDocument($context_document_srl); if ($oDocument && $oDocument->isExists()) { // Document 객체를 stdClass로 변환 $doc = new stdClass(); $doc->document_srl = $oDocument->document_srl; $doc->title = $oDocument->getTitleText(); $doc->content = $oDocument->getContent(); $doc->nick_name = $oDocument->getNickName(); $doc->member_srl = $oDocument->get('member_srl'); $doc->regdate = $oDocument->getRegdate(); $doc->module_srl = $oDocument->get('module_srl'); // 댓글 조회 $oCommentModel = getModel('comment'); $comment_list = $oCommentModel->getCommentList($context_document_srl); if ($comment_list && $comment_list->data) { $comments = $comment_list->data; } // 권한 조회 $oModuleModel = getModel('module'); $module_info = $oModuleModel->getModuleInfoByModuleSrl($doc->module_srl); $logged_info = Context::get('logged_info'); $api_grant = $oModuleModel->getGrant($module_info, $logged_info); } } $can_write_comment = $api_grant && ($api_grant->write_comment ?? false); @endphp ``` #### HTML 부분 (댓글 폼 - AJAX용) ```html {{-- 댓글 작성 영역 --}} @if($can_write_comment)
댓글 등록
@else

댓글을 작성하려면 로그인이 필요합니다.

@endif ``` #### JavaScript 부분 (REST API 호출) ```html // 전역 변수 var documentSrl = {{ $doc->document_srl ?? 0 }}; // 댓글 등록 window.submitNewComment = async function() { var content = document.getElementById('comment-textarea').value.trim(); if (!content) { alert('댓글 내용을 입력하세요.'); return; } var btn = document.getElementById('comment-submit-btn'); btn.disabled = true; btn.textContent = '등록 중...'; try { var response = await fetch('/modules/api/rest.php?type=comment_insert&document_srl=' + documentSrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: content }), credentials: 'same-origin' // 세션 쿠키 전송 필수! }); var ret = await response.json(); if (ret.status === 1) { alert('댓글이 등록되었습니다.'); location.reload(); // 또는 동적으로 댓글 추가 } else { alert(ret.message || '댓글 등록에 실패했습니다.'); } } catch (error) { console.error('Error:', error); alert('오류가 발생했습니다.'); } finally { btn.disabled = false; btn.textContent = '댓글 등록'; } }; // 댓글 수정 window.submitEditComment = async function(commentSrl) { var content = document.getElementById('comment-edit-content-' + commentSrl).value.trim(); if (!content) { alert('댓글 내용을 입력하세요.'); return; } try { var response = await fetch('/modules/api/rest.php?type=comment_update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ comment_srl: commentSrl, content: content }), credentials: 'same-origin' }); var ret = await response.json(); if (ret.status === 1) { // 성공 - UI 업데이트 document.getElementById('comment-content-' + commentSrl).innerHTML = content.replace(/\n/g, '
'); cancelEditComment(commentSrl); } else { alert(ret.message || '수정에 실패했습니다.'); } } catch (error) { alert('오류가 발생했습니다.'); } }; // 댓글 삭제 window.deleteComment = async function(commentSrl) { if (!confirm('댓글을 삭제하시겠습니까?')) return; try { var response = await fetch('/modules/api/rest.php?type=comment_delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ comment_srl: commentSrl }), credentials: 'same-origin' }); var ret = await response.json(); if (ret.status === 1) { // 성공 - DOM에서 댓글 제거 var commentEl = document.querySelector('[data-comment-srl="' + commentSrl + '"]'); if (commentEl) commentEl.remove(); } else { alert(ret.message || '삭제에 실패했습니다.'); } } catch (error) { alert('오류가 발생했습니다.'); } }; ``` #### REST API 서버 코드 (`modules/api/rest.php`) ```php member_srl) { jsonResponse(-1, '로그인이 필요합니다.'); } // 문서 존재 확인 $oDocumentModel = getModel('document'); $oDocument = $oDocumentModel->getDocument($document_srl); if (!$oDocument || !$oDocument->isExists()) { jsonResponse(-1, '존재하지 않는 게시글입니다.'); } $module_srl = $oDocument->get('module_srl'); $content = $_POST['content'] ?? ''; $parent_srl = $_POST['parent_srl'] ?? 0; if (empty($content)) { jsonResponse(-1, '댓글 내용을 입력하세요.'); } // 댓글 권한 체크 $oModuleModel = getModel('module'); $module_info = $oModuleModel->getModuleInfoByModuleSrl($module_srl); $grant = $oModuleModel->getGrant($module_info, $logged_info); if (!$grant->write_comment) { jsonResponse(-1, '댓글 작성 권한이 없습니다.'); } // comment_srl 생성 $comment_srl = getNextSequence(); // 댓글 삽입 $args = new stdClass(); $args->comment_srl = $comment_srl; $args->module_srl = $module_srl; $args->document_srl = $document_srl; $args->parent_srl = $parent_srl; $args->content = $content; $args->member_srl = $logged_info->member_srl; $args->nick_name = $logged_info->nick_name; $args->user_id = $logged_info->user_id; $args->user_name = $logged_info->user_name; $args->email_address = $logged_info->email_address; $args->regdate = date('YmdHis'); $args->last_update = date('YmdHis'); $args->ipaddress = $_SERVER['REMOTE_ADDR']; $args->list_order = $parent_srl ? $parent_srl : ($comment_srl * -1); $args->status = 1; $output = executeQuery('comment.insertComment', $args); if ($output->toBool()) { // 문서의 댓글 수 업데이트 $oCommentController = getController('comment'); $oCommentController->updateCommentCount($document_srl); jsonResponse(1, '댓글이 등록되었습니다.', ['comment_srl' => $comment_srl]); } else { jsonResponse(-1, '댓글 등록에 실패했습니다.'); } break; ``` --- ## 보안 비교 | 항목 | 방법 1 (모듈 컨텍스트) | 방법 2 (REST API) | |------|------------------------|-------------------| | **CSRF 방지** | `_rx_csrf_token` 자동 검증 | 세션 기반 (credentials: same-origin) | | **권한 체크** | 게시판 모듈이 자동 처리 | 직접 구현 (`$grant->write_comment`) | | **SQL Injection** | executeQuery (prepared statement) | executeQuery (prepared statement) | | **XSS 방지** | Rhymix 기본 필터링 | 직접 이스케이프 필요 | | **인증** | 세션 자동 확인 | 세션 수동 확인 필요 | ### 방법 1 보안 장점 - Rhymix 코어가 모든 보안 처리 담당 - 별도 구현 불필요 - 검증된 보안 로직 사용 ### 방법 2 보안 주의사항 ```php // rest.php에서 반드시 구현해야 할 보안 체크 // 1. 로그인 체크 $logged_info = Context::get('logged_info'); if (!$logged_info || !$logged_info->member_srl) { jsonResponse(-1, '로그인이 필요합니다.'); } // 2. 게시글 존재 확인 $oDocument = $oDocumentModel->getDocument($document_srl); if (!$oDocument || !$oDocument->isExists()) { jsonResponse(-1, '존재하지 않는 게시글입니다.'); } // 3. 권한 체크 $grant = $oModuleModel->getGrant($module_info, $logged_info); if (!$grant->write_comment) { jsonResponse(-1, '댓글 작성 권한이 없습니다.'); } // 4. 수정/삭제 시 작성자 확인 $oComment = $oCommentModel->getComment($comment_srl); if (!$oComment->isGranted()) { jsonResponse(-1, '권한이 없습니다.'); } ``` --- ## 사용성 비교 | 항목 | 방법 1 (모듈 컨텍스트) | 방법 2 (REST API) | |------|------------------------|-------------------| | **페이지 리로드** | 항상 발생 | 없음 (AJAX) | | **사용자 경험** | 전통적 웹 | SPA 스타일 | | **로딩 속도** | 전체 페이지 로드 | 부분 업데이트 | | **에러 피드백** | 페이지 이동 후 표시 | 즉시 알림 | | **구현 복잡도** | 간단 | 복잡 | | **htmx 호환** | 제한적 | 완벽 호환 | ### 방법 1 사용성 장점 - 구현이 간단함 - 브라우저 뒤로가기 자연스러움 - SEO 친화적 ### 방법 2 사용성 장점 - 빠른 응답 (페이지 리로드 없음) - 부분 UI 업데이트 가능 - 현대적인 사용자 경험 - htmx, Alpine.js 등과 잘 연동 --- ## 선택 가이드 ### 방법 1 선택 시 - 간단한 게시판 구현 - 빠른 개발 필요 - Rhymix 기본 기능 활용 - 보안 구현에 자신 없을 때 ### 방법 2 선택 시 - SPA 스타일 동적 UI - htmx 사용 - 커스텀 데이터 처리 필요 - 페이지 리로드 없이 UX 개선 --- ## 문제 해결 ### "exec_json 성공인데 댓글이 안 보여요" **원인**: 레이아웃에서 AJAX로 procBoardInsertComment 호출 시 모듈 컨텍스트 없음 **해결**: REST API 방식 사용 ### "credentials 빠뜨렸더니 로그인 안 된다고 해요" ```javascript // 잘못된 코드 fetch('/modules/api/rest.php?type=comment_insert', { method: 'POST' }); // 올바른 코드 fetch('/modules/api/rest.php?type=comment_insert', { method: 'POST', credentials: 'same-origin' // 필수! }); ``` ### "권한이 없습니다" 1. 게시판 관리 → 권한 설정에서 댓글 권한 확인 2. 로그인 상태 확인 3. rest.php에서 권한 체크 로직 확인 --- ## 참고 파일 | 파일 | 설명 | |------|------| | `layouts/el_d1/assets/pages/community_view.blade.php` | 방법 1 구현 예제 | | `layouts/el_d1/assets/pages/homepage_solution_view.blade.php` | 방법 2 구현 예제 | | `modules/api/rest.php` | REST API 서버 | | `modules/api/CLAUDE.md` | API 상세 문서 | --- *최종 업데이트: 2025-12-01*
0 좋아요 0 답글 296 조회