./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 조회