docs(core,extensions): 확장 개발자 문서 9세트 집필과 생성기 정합 보강
S1 파일럿 3종(sirsoft-board · sirsoft-gdpr · sirsoft-admin_basic)에 이어
결제·본인인증 동형군 6종의 AGENTS.md · README.md · docs/ 5문서를 집필했다.
확장을 고치려는 쪽이 매번 src/ 를 훑어 구조를 재발견하지 않도록, 설계 의도와
확장점(발행·구독 훅)·수정 시 동반 의무·금지 패턴을 코드 근거로 서술했다.
생성기(ExtensionDocScaffolder)에서 표를 무의미하게 만들던 세 결함을 함께 고쳤다.
getLayoutExtensions 기본 구현이 돌려주는 절대경로가 파일 목록과 중복돼 로컬
머신 경로가 커밋 문서에 실리던 문제, getNotificationDefinitions 를 'key'/'event'
로 읽어 모든 행이 '-' 로 찍히던 문제, getSettingsLayout 절대경로가 정규화 없이
노출되던 문제다. 셋 다 예외를 남기지 않고 표만 조용히 망가뜨리므로
ExtensionDocContractTest 에 각각의 되돌림 red 를 확인한 단언을 두었다.
템플릿의 extensions/{id}/ 는 모듈·플러그인의 발행과 반대 방향(오버라이드)이라
별도 블록(template-overrides)으로 분리했다.
sirsoft-admin_basic 의 컴포넌트·핸들러·레이아웃 문서를 코어 docs/ 에서 그 템플릿
소유로 이관하고, 남은 참조 6축을 재는 가드를 추가했다. 이관 후 남은 옛 경로는
오류가 아니라 헛걸음으로만 나타나 드러나지 않는다. 의 상대 링크가
한 단계 얕아 공유 docs/ 대신 를 가리키던 문제도 함께 고쳤다.
README 상단의 확장명 이미지 배지를 평문 H1 로 바꿨다( 지시 2026-08-31).
루트 README.md · README.ko.md 도 같은 기준을 적용했다. 정보 배지는 유지한다.
sirsoft-gdpr: 회원탈퇴로 자동 철회된 동의가 관리자 동의 이력 화면의 출처 필터로
걸러지지 않던 문제를 고쳤다. Repository 가 'withdraw' 리터럴을 직접 UPDATE 에
싣는데 그 값이 ConsentSource enum 에 없어, 화면 필터 옵션·라벨 어느 쪽에도
도달하지 못했다. 어휘를 enum 단일 출처로 모으고 ko·en·ja 라벨과 필터 옵션을
함께 채웠으며, 어휘 대조 테스트의 모집단에 Repository 를 편입했다.
This commit is contained in:
@@ -47,7 +47,7 @@
|
||||
| [user-overrides.md](docs/backend/user-overrides.md) | 사용자 수정 보존 (HasUserOverrides Trait) | 모델에 `use HasUserOverrides;` + `protected array $trackable... |
|
||||
| [validation.md](docs/backend/validation.md) | 검증 (Validation) | 필수: FormRequest에서 검증 (Service에 검증 로직 배치 금지) |
|
||||
|
||||
### 프론트엔드 [frontend/](docs/frontend/) (50개)
|
||||
### 프론트엔드 [frontend/](docs/frontend/) (47개)
|
||||
|
||||
| 문서 | 설명 | TL;DR 핵심 |
|
||||
|------|------|-----------|
|
||||
@@ -95,9 +95,6 @@
|
||||
| [tailwind-safelist.md](docs/frontend/tailwind-safelist.md) | Tailwind Safelist 가이드 | Tailwind는 빌드 시 사용된 클래스만 CSS에 포함 |
|
||||
| [template-development.md](docs/frontend/template-development.md) | 템플릿 개발 가이드라인 | 디렉토리: templates/[vendor-template]/ (예: sirsoft-admin_basic) |
|
||||
| [template-handlers.md](docs/frontend/template-handlers.md) | 템플릿 전용 핸들러 | setLocale: 앱 언어 변경 — 엔진 빌트인 (ActionDispatcher) |
|
||||
| [components.md](docs/frontend/templates/sirsoft-admin_basic/components.md) | sirsoft-admin_basic 컴포넌트 | Basic 37개: HTML 래핑 (Div, Button, Input, Select, Form, A, ... |
|
||||
| [handlers.md](docs/frontend/templates/sirsoft-admin_basic/handlers.md) | sirsoft-admin_basic 핸들러 | setLocale: 앱 언어 변경 (locale 파라미터) |
|
||||
| [layouts.md](docs/frontend/templates/sirsoft-admin_basic/layouts.md) | sirsoft-admin_basic 레이아웃 | 베이스: _admin_base.json (사이드바 + 헤더 + 콘텐츠 슬롯) |
|
||||
| [components.md](docs/frontend/templates/sirsoft-basic/components.md) | sirsoft-basic 컴포넌트 | Basic 26개: HTML 래핑 (Div, Button, Input, Select, Form, A, ... |
|
||||
| [handlers.md](docs/frontend/templates/sirsoft-basic/handlers.md) | sirsoft-basic 핸들러 | setTheme/initTheme: 다크/라이트 모드 전환 (admin과 동일 키 공유) |
|
||||
| [layouts.md](docs/frontend/templates/sirsoft-basic/layouts.md) | sirsoft-basic 레이아웃 | 베이스: _user_base.json (헤더 + 푸터 + 모바일 네비 + 콘텐츠 슬롯) |
|
||||
@@ -180,6 +177,23 @@
|
||||
| `sirsoft-verification_nhnkcp` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-verification_nhnkcp/docs/api/README.md) | 1 / 1 |
|
||||
|
||||
|
||||
### 확장 개발자 문서 (9개 확장, 자동 스캔)
|
||||
|
||||
> 확장을 수정하기 전에 읽는 문서. 설계 의도 · 디렉토리 지도 · 확장점(발행/구독 훅) · 수정 시 동반 의무 · 금지 패턴을 담는다. `php artisan ext:docgen` 이 실측 부분을 유지하며, 이 표는 `{modules,plugins,templates}/_bundled/*/docs/README.md` 를 패턴 스캔해 자동 편입된다(확장명 하드코딩 없음).
|
||||
|
||||
| 확장 | 유형 | 에이전트 가이드 | 문서 목차 | 실측 집계 |
|
||||
|------|------|----------------|----------|----------|
|
||||
| `sirsoft-board` | 모듈 | [AGENTS.md](modules/_bundled/sirsoft-board/AGENTS.md) | [docs/](modules/_bundled/sirsoft-board/docs/README.md) | 훅 90 · 라우트 80 · 모델 9 · 레이아웃 46 |
|
||||
| `sirsoft-gdpr` | 플러그인 | [AGENTS.md](plugins/_bundled/sirsoft-gdpr/AGENTS.md) | [docs/](plugins/_bundled/sirsoft-gdpr/docs/README.md) | 훅 2 · 라우트 15 · 모델 3 · 레이아웃 4 |
|
||||
| `sirsoft-pay_kginicis` | 플러그인 | [AGENTS.md](plugins/_bundled/sirsoft-pay_kginicis/AGENTS.md) | [docs/](plugins/_bundled/sirsoft-pay_kginicis/docs/README.md) | 훅 6 · 라우트 35 · 모델 0 · 레이아웃 1 |
|
||||
| `sirsoft-pay_nhnkcp` | 플러그인 | [AGENTS.md](plugins/_bundled/sirsoft-pay_nhnkcp/AGENTS.md) | [docs/](plugins/_bundled/sirsoft-pay_nhnkcp/docs/README.md) | 훅 8 · 라우트 16 · 모델 0 · 레이아웃 1 |
|
||||
| `sirsoft-pay_nicepayments` | 플러그인 | [AGENTS.md](plugins/_bundled/sirsoft-pay_nicepayments/AGENTS.md) | [docs/](plugins/_bundled/sirsoft-pay_nicepayments/docs/README.md) | 훅 5 · 라우트 15 · 모델 0 · 레이아웃 1 |
|
||||
| `sirsoft-tosspayments` | 플러그인 | [AGENTS.md](plugins/_bundled/sirsoft-tosspayments/AGENTS.md) | [docs/](plugins/_bundled/sirsoft-tosspayments/docs/README.md) | 훅 4 · 라우트 4 · 모델 0 · 레이아웃 1 |
|
||||
| `sirsoft-verification_kginicis` | 플러그인 | [AGENTS.md](plugins/_bundled/sirsoft-verification_kginicis/AGENTS.md) | [docs/](plugins/_bundled/sirsoft-verification_kginicis/docs/README.md) | 훅 3 · 라우트 2 · 모델 2 · 레이아웃 1 |
|
||||
| `sirsoft-verification_nhnkcp` | 플러그인 | [AGENTS.md](plugins/_bundled/sirsoft-verification_nhnkcp/AGENTS.md) | [docs/](plugins/_bundled/sirsoft-verification_nhnkcp/docs/README.md) | 훅 0 · 라우트 2 · 모델 2 · 레이아웃 1 |
|
||||
| `sirsoft-admin_basic` | 템플릿 | [AGENTS.md](templates/_bundled/sirsoft-admin_basic/AGENTS.md) | [docs/](templates/_bundled/sirsoft-admin_basic/docs/README.md) | 훅 0 · 라우트 29 · 모델 0 · 레이아웃 145 |
|
||||
|
||||
|
||||
<!-- AUTO-GENERATED-END: docs-quick-reference -->
|
||||
|
||||
---
|
||||
|
||||
+3
-7
@@ -1,13 +1,9 @@
|
||||
<p align="center"><a href="README.md">English</a> | 한국어</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/그누보드7-Gnuboard7-000000?style=for-the-badge&labelColor=0066FF&logoColor=white" height="200" alt="그누보드7 (Gnuboard7)">
|
||||
</p>
|
||||
# 그누보드7
|
||||
|
||||
<p align="center">
|
||||
<strong>모던 아키텍처로 다시 태어난 대한민국 대표 오픈소스 CMS</strong><br>
|
||||
A modern, extensible CMS platform built with Laravel + React
|
||||
</p>
|
||||
**모던 아키텍처로 다시 태어난 대한민국 대표 오픈소스 CMS**
|
||||
A modern, extensible CMS platform built with Laravel + React
|
||||
|
||||
<p align="center">
|
||||
<a href="#"><img src="https://img.shields.io/badge/version-7.0.10-blue" alt="Version"></a>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<p align="center">English | <a href="README.ko.md">한국어</a></p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Gnuboard7-그누보드7-000000?style=for-the-badge&labelColor=0066FF&logoColor=white" height="200" alt="Gnuboard7 (그누보드7)">
|
||||
</p>
|
||||
# Gnuboard7
|
||||
|
||||
<p align="center">
|
||||
<strong>A modern, extensible CMS platform built with Laravel + React</strong><br>
|
||||
The next generation of Gnuboard — Korea's most widely used open-source CMS
|
||||
</p>
|
||||
**A modern, extensible CMS platform built with Laravel + React**
|
||||
The next generation of Gnuboard — Korea's most widely used open-source CMS
|
||||
|
||||
<p align="center">
|
||||
<a href="#"><img src="https://img.shields.io/badge/version-7.0.10-blue" alt="Version"></a>
|
||||
|
||||
@@ -149,6 +149,11 @@ class ExtensionDocScaffolder
|
||||
'blocks' => [
|
||||
'레이아웃 목록' => 'layouts',
|
||||
'라우트 매핑' => 'layout-map',
|
||||
// 템플릿의 `extensions/{module-identifier}/*.json` 은 그 모듈/플러그인이
|
||||
// 발행한 레이아웃 확장 조각을 이 템플릿이 오버라이드한 것이다(모듈/플러그인
|
||||
// 쪽 `resources/extensions/` 와는 반대 방향 — 발행이 아니라 대체). 모듈/플러그인
|
||||
// 문서의 `docs/extension-points.md` 는 템플릿에 존재하지 않아 자리가 없었다.
|
||||
'확장 오버라이드' => 'template-overrides',
|
||||
],
|
||||
],
|
||||
'docs/handlers.md' => [
|
||||
@@ -538,6 +543,7 @@ class ExtensionDocScaffolder
|
||||
'hooks-subscribed' => $this->renderHooksSubscribed($ctx),
|
||||
'listeners' => $this->renderListeners($ctx),
|
||||
'layout-extensions' => $this->renderLayoutExtensions($ctx),
|
||||
'template-overrides' => $this->renderLayoutExtensions($ctx),
|
||||
'middleware' => $this->renderMiddleware($ctx),
|
||||
'channels' => $this->renderChannels($ctx),
|
||||
'schedules' => $this->renderSchedules($ctx),
|
||||
@@ -963,6 +969,7 @@ class ExtensionDocScaffolder
|
||||
['제공 컴포넌트', (string) $frontend['components']['total'].'개', $this->docLink($ctx, 'docs/components.md', '제공 컴포넌트')],
|
||||
['레이아웃', (string) count($frontend['layouts']).'개', $this->docLink($ctx, 'docs/layouts.md', '레이아웃 목록')],
|
||||
['전용 핸들러', (string) count($frontend['handlers']['names']).'개', $this->docLink($ctx, 'docs/handlers.md', '템플릿 전용 핸들러')],
|
||||
['확장 오버라이드', (string) count($frontend['layoutExtensions']).'개', $this->docLink($ctx, 'docs/layouts.md', '확장 오버라이드')],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1250,23 +1257,37 @@ class ExtensionDocScaffolder
|
||||
{
|
||||
$files = $ctx['frontend']['layoutExtensions'];
|
||||
$declared = $ctx['surface']['values']['getLayoutExtensions'] ?? [];
|
||||
// 템플릿의 `extensions/{module-identifier}/*.json` 은 그 모듈/플러그인이 발행한
|
||||
// 조각을 이 템플릿이 대체하는 것이지, 모듈/플러그인처럼 밖으로 발행하는 것이 아니다.
|
||||
$isTemplate = ($ctx['record']['type'] ?? null) === ExtensionInventory::TYPE_TEMPLATE;
|
||||
|
||||
if ($files === [] && ! is_array($declared)) {
|
||||
return $this->none('레이아웃 확장이 없습니다.');
|
||||
return $this->none($isTemplate ? '오버라이드하는 레이아웃 확장 조각이 없습니다.' : '레이아웃 확장이 없습니다.');
|
||||
}
|
||||
|
||||
if ($files === [] && $declared === []) {
|
||||
return $this->none('레이아웃 확장이 없습니다.');
|
||||
return $this->none($isTemplate ? '오버라이드하는 레이아웃 확장 조각이 없습니다.' : '레이아웃 확장이 없습니다.');
|
||||
}
|
||||
|
||||
$known = array_flip($files);
|
||||
|
||||
$rows = [];
|
||||
foreach ($files as $file) {
|
||||
$rows[] = [$this->code($file), '다른 확장/템플릿 레이아웃에 주입되는 조각'];
|
||||
$rows[] = [$this->code($file), $isTemplate ? '모듈/플러그인 확장 조각을 대체하는 오버라이드' : '다른 확장/템플릿 레이아웃에 주입되는 조각'];
|
||||
}
|
||||
|
||||
// `getLayoutExtensions()` 기본 구현(AbstractModule/AbstractPlugin)은 위 파일 목록과
|
||||
// 같은 디렉토리를 glob() 한 절대경로라 100% 중복이다. 확장-상대 경로로 정규화한 뒤
|
||||
// 이미 실린 파일은 건너뛰고, 확장이 오버라이드해 다른 대상을 선언한 경우만 싣는다.
|
||||
if (is_array($declared)) {
|
||||
foreach ($declared as $key => $value) {
|
||||
$rows[] = [$this->code(is_string($key) ? $key : (string) $value), '`getLayoutExtensions()` 선언'];
|
||||
$raw = is_string($key) ? $key : (is_string($value) ? $value : (string) $value);
|
||||
$rel = $this->relativeToExtension($ctx, $raw);
|
||||
if (isset($known[$rel])) {
|
||||
continue;
|
||||
}
|
||||
$known[$rel] = true;
|
||||
$rows[] = [$this->code($rel), '`getLayoutExtensions()` 선언'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1372,7 +1393,11 @@ class ExtensionDocScaffolder
|
||||
|
||||
$rows = [];
|
||||
foreach ($definitions as $key => $definition) {
|
||||
$name = is_string($key) ? $key : (is_array($definition) ? ($definition['key'] ?? $definition['event'] ?? '-') : (string) $definition);
|
||||
// getNotificationDefinitions() 의 표준 계약(AbstractModule/AbstractPlugin 소비처인
|
||||
// NotificationSyncHelper 기준)은 리스트 배열 + 각 원소의 'type' 키다. 'key'/'event'
|
||||
// 는 어떤 확장도 쓰지 않아, 문자열 키가 아닌 한(list 배열이면 전부 정수 키) 이 표는
|
||||
// 항상 '-' 만 찍고 있었다.
|
||||
$name = is_string($key) ? $key : (is_array($definition) ? ($definition['type'] ?? $definition['key'] ?? $definition['event'] ?? '-') : (string) $definition);
|
||||
$channels = is_array($definition) ? ($definition['channels'] ?? null) : null;
|
||||
|
||||
$rows[] = [
|
||||
@@ -1590,7 +1615,7 @@ class ExtensionDocScaffolder
|
||||
$extra[] = '기본값 파일: '.$this->code($this->relativeToExtension($ctx, $defaultsPath));
|
||||
}
|
||||
if (is_string($layout) && $layout !== '') {
|
||||
$extra[] = '설정 화면 레이아웃: '.$this->code($layout);
|
||||
$extra[] = '설정 화면 레이아웃: '.$this->code($this->relativeToExtension($ctx, $layout));
|
||||
}
|
||||
|
||||
if ($extra !== []) {
|
||||
@@ -1624,7 +1649,7 @@ class ExtensionDocScaffolder
|
||||
$layout = $ctx['surface']['values']['getSettingsLayout'] ?? null;
|
||||
|
||||
if (is_string($layout) && $layout !== '') {
|
||||
return '관리자 설정 화면이 있습니다 (레이아웃: '.$this->code($layout).'). 설정 항목은 화면에서 확인하세요.'
|
||||
return '관리자 설정 화면이 있습니다 (레이아웃: '.$this->code($this->relativeToExtension($ctx, $layout)).'). 설정 항목은 화면에서 확인하세요.'
|
||||
.(is_string($route) && $route !== '' ? ' 경로: '.$this->code($route) : '');
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -10,7 +10,7 @@
|
||||
| 카테고리 | 문서 수 | 링크 상태 |
|
||||
|----------|---------|----------|
|
||||
| [백엔드](backend/) | 37개 | 정상 |
|
||||
| [프론트엔드](frontend/) | 51개 | 정상 |
|
||||
| [프론트엔드](frontend/) | 49개 | 정상 |
|
||||
| [확장 시스템](extension/) | 32개 | 정상 |
|
||||
| 공통 | 20개 | 정상 |
|
||||
| [AI 도구](ai-tools/) | - | 정상 |
|
||||
@@ -43,7 +43,7 @@ G7 이 제공하는 REST API 의 엔드포인트별 요청 파라미터·응답
|
||||
| 4 | [레이아웃 JSON - 상속](frontend/layout-json-inheritance.md) | extends: 베이스 레이아웃 상속 (type: "slot" 위치에 삽입) |
|
||||
| 5 | [컴포넌트 개발 규칙](frontend/components.md) | HTML 태그 직접 사용 금지 |
|
||||
| 6 | [컴포넌트 Props 레퍼런스](frontend/component-props.md) | - |
|
||||
| 7 | [sirsoft-admin_basic 컴포넌트](frontend/templates/sirsoft-admin_basic/components.md) | Basic (37개), Composite (66개), Layout (8개) |
|
||||
| 7 | [sirsoft-basic 컴포넌트](frontend/templates/sirsoft-basic/components.md) | Basic (26개), Composite 등 |
|
||||
| 8 | [데이터 바인딩 및 표현식](frontend/data-binding.md) | API 데이터: {{user.name}}, URL 파라미터: {{route.id}} |
|
||||
| 9 | [데이터 바인딩 - 다국어 처리](frontend/data-binding-i18n.md) | - |
|
||||
| 10 | [액션 핸들러 가이드](frontend/actions.md) | 구조: type 또는 event(이벤트), handler(핸들러명), params(옵션) |
|
||||
@@ -52,6 +52,8 @@ G7 이 제공하는 REST API 의 엔드포인트별 요청 파라미터·응답
|
||||
| 13 | [데이터 소스](frontend/data-sources.md) | data_sources 배열에 API 정의: id, endpoint, method |
|
||||
| 14 | [다크 모드 지원](frontend/dark-mode.md) | Tailwind dark: variant 사용 |
|
||||
|
||||
sirsoft-admin_basic 컴포넌트 문서는 확장이 소유합니다 — [templates/_bundled/sirsoft-admin_basic/docs/components.md](../templates/_bundled/sirsoft-admin_basic/docs/components.md) 를 참고하세요.
|
||||
|
||||
### 컨트롤러 작성
|
||||
|
||||
| 순서 | 문서 | TL;DR 핵심 |
|
||||
@@ -167,7 +169,7 @@ G7 이 제공하는 REST API 의 엔드포인트별 요청 파라미터·응답
|
||||
| [user-overrides.md](backend/user-overrides.md) | 사용자 수정 보존 (HasUserOverrides Trait) |
|
||||
| [validation.md](backend/validation.md) | 검증 (Validation) |
|
||||
|
||||
### 프론트엔드 (51개)
|
||||
### 프론트엔드 (49개)
|
||||
|
||||
| 문서 | 제목 |
|
||||
|------|------|
|
||||
@@ -216,9 +218,7 @@ G7 이 제공하는 REST API 의 엔드포인트별 요청 파라미터·응답
|
||||
| [tailwind-safelist.md](frontend/tailwind-safelist.md) | Tailwind Safelist 가이드 |
|
||||
| [template-development.md](frontend/template-development.md) | 템플릿 개발 가이드라인 |
|
||||
| [template-handlers.md](frontend/template-handlers.md) | 템플릿 전용 핸들러 |
|
||||
| [components.md](frontend/components.md) | sirsoft-admin_basic 컴포넌트 |
|
||||
| [handlers.md](frontend/handlers.md) | sirsoft-admin_basic 핸들러 |
|
||||
| [layouts.md](frontend/layouts.md) | sirsoft-admin_basic 레이아웃 |
|
||||
| [README.md](frontend/README.md) | 템플릿별 컴포넌트·핸들러·레이아웃 문서 |
|
||||
| [components.md](frontend/components.md) | sirsoft-basic 컴포넌트 |
|
||||
| [handlers.md](frontend/handlers.md) | sirsoft-basic 핸들러 |
|
||||
| [layouts.md](frontend/layouts.md) | sirsoft-basic 레이아웃 |
|
||||
|
||||
@@ -173,7 +173,7 @@ global.window = {
|
||||
- docs/frontend/template-development.md
|
||||
- docs/extension/template-basics.md
|
||||
- docs/extension/template-commands.md
|
||||
- docs/frontend/templates/sirsoft-admin_basic/components.md
|
||||
- templates/_bundled/sirsoft-admin_basic/docs/components.md
|
||||
- docs/frontend/templates/sirsoft-basic/components.md
|
||||
|
||||
## 테스트 실행
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
|
||||
| 템플릿 식별자 | 컴포넌트 | 핸들러 | 레이아웃 |
|
||||
|--------------|---------|--------|--------|
|
||||
| `sirsoft-admin_basic` | [components.md](templates/sirsoft-admin_basic/components.md) | [handlers.md](templates/sirsoft-admin_basic/handlers.md) | [layouts.md](templates/sirsoft-admin_basic/layouts.md) |
|
||||
| `sirsoft-basic` | [components.md](templates/sirsoft-basic/components.md) | [handlers.md](templates/sirsoft-basic/handlers.md) | [layouts.md](templates/sirsoft-basic/layouts.md) |
|
||||
|
||||
### 컴포넌트 개발
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 컴포넌트 Props 레퍼런스 - Composite
|
||||
|
||||
> **관련 문서**: [컴포넌트 Props (Basic/DataGrid/Modal)](component-props.md) | [컴포넌트 개발 규칙](components.md) | [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)
|
||||
> **관련 문서**: [컴포넌트 Props (Basic/DataGrid/Modal)](component-props.md) | [컴포넌트 개발 규칙](components.md) | [sirsoft-admin_basic 컴포넌트](../../templates/_bundled/sirsoft-admin_basic/docs/components.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -671,7 +671,7 @@ G7에서는 콘텐츠의 렌더링 모드를 DB의 `*_mode` 컬럼(`'text'` / `'
|
||||
- [컴포넌트 Props (Basic/DataGrid/Modal)](component-props.md)
|
||||
- [컴포넌트 개발 규칙](components.md)
|
||||
- [컴포넌트 고급 기능](components-advanced.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](../../templates/_bundled/sirsoft-admin_basic/docs/components.md)
|
||||
- [액션 핸들러 - 커스텀 콜백](actions.md#커스텀-이벤트-event-필드)
|
||||
- [보안 가이드 - HTML 렌더링](security.md#htmlcontent--htmleditor-html-렌더링이-필요한-경우)
|
||||
- [에디터 컴포넌트](editors.md)
|
||||
|
||||
@@ -1144,7 +1144,7 @@ id prop 사용: scrollIntoView 등 DOM selector로 접근해야 할 때 필수
|
||||
|
||||
- [컴포넌트 개발 규칙](components.md) - basic, composite, layout 컴포넌트
|
||||
- [레이아웃 JSON 스키마](layout-json.md) - 컴포넌트를 레이아웃 JSON에서 사용하는 방법
|
||||
- [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md) - Admin 컴포넌트 목록 (111개)
|
||||
- [sirsoft-admin_basic 컴포넌트](../../templates/_bundled/sirsoft-admin_basic/docs/components.md) - Admin 컴포넌트 목록
|
||||
- [sirsoft-basic 컴포넌트](templates/sirsoft-basic/components.md) - User 컴포넌트 목록 (58개)
|
||||
- [에디터 컴포넌트](editors.md) - HtmlEditor, CodeEditor 상세 가이드
|
||||
- [데이터 바인딩](data-binding.md) - `{{}}` 표현식, `$t:` 다국어
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 컴포넌트 타입별 개발 규칙
|
||||
|
||||
> **메인 문서**: [components.md](components.md)
|
||||
> **관련 문서**: [layout-json-components.md](layout-json-components.md) | [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)
|
||||
> **관련 문서**: [layout-json-components.md](layout-json-components.md) | [sirsoft-admin_basic 컴포넌트](../../templates/_bundled/sirsoft-admin_basic/docs/components.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -452,5 +452,5 @@ export const Container: React.FC<ContainerProps> = ({ children }) => (
|
||||
- [컴포넌트 개발 규칙 인덱스](components.md)
|
||||
- [컴포넌트 패턴](components-patterns.md)
|
||||
- [컴포넌트 고급 기능](components-advanced.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](../../templates/_bundled/sirsoft-admin_basic/docs/components.md)
|
||||
- [sirsoft-basic 컴포넌트](templates/sirsoft-basic/components.md)
|
||||
|
||||
@@ -29,11 +29,13 @@
|
||||
|
||||
| 템플릿 식별자 | 컴포넌트 | 핸들러 | 레이아웃 |
|
||||
|--------------|---------|--------|--------|
|
||||
| `sirsoft-admin_basic` | [components.md](templates/sirsoft-admin_basic/components.md) | [handlers.md](templates/sirsoft-admin_basic/handlers.md) | [layouts.md](templates/sirsoft-admin_basic/layouts.md) |
|
||||
| `sirsoft-basic` | [components.md](templates/sirsoft-basic/components.md) | [handlers.md](templates/sirsoft-basic/handlers.md) | [layouts.md](templates/sirsoft-basic/layouts.md) |
|
||||
|
||||
<!-- AUTO-GENERATED-END: frontend-template-reference -->
|
||||
|
||||
> `sirsoft-admin_basic` 은 문서를 그 템플릿이 직접 소유합니다 — [templates/_bundled/sirsoft-admin_basic/docs/](../../templates/_bundled/sirsoft-admin_basic/docs/README.md).
|
||||
> 템플릿별 문서 위치는 [templates/README.md](templates/README.md) 를 따릅니다.
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
@@ -153,7 +155,7 @@ import { Icon, IconName } from '../basic/Icon';
|
||||
- [g7core-api.md](g7core-api.md) - G7Core 전역 API 레퍼런스
|
||||
- [레이아웃 JSON 스키마](./layout-json.md) - 컴포넌트를 레이아웃 JSON에서 사용하는 방법
|
||||
- [데이터 바인딩](./data-binding.md) - props에서 데이터 바인딩 사용법
|
||||
- [sirsoft-admin_basic 컴포넌트](./templates/sirsoft-admin_basic/components.md) - Admin 컴포넌트 목록 (111개)
|
||||
- [sirsoft-admin_basic 컴포넌트](../../templates/_bundled/sirsoft-admin_basic/docs/components.md) - Admin 컴포넌트 목록
|
||||
- [sirsoft-basic 컴포넌트](./templates/sirsoft-basic/components.md) - User 컴포넌트 목록 (58개)
|
||||
- [다크 모드](./dark-mode.md) - 컴포넌트 다크 모드 지원 가이드
|
||||
- [상태 관리](./state-management.md) - 전역/로컬 상태 관리 및 동기화 패턴
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
| `sirsoft-admin_basic` | 미선언 | ThemeToggle 존재, dark: variant 동작 |
|
||||
| `sirsoft-basic` | `true` | template.json에 선언, 완전 지원 |
|
||||
|
||||
> 상세 컴포넌트 목록: [sirsoft-admin_basic](templates/sirsoft-admin_basic/components.md), [sirsoft-basic](templates/sirsoft-basic/components.md)
|
||||
> 상세 컴포넌트 목록: [sirsoft-admin_basic](../../templates/_bundled/sirsoft-admin_basic/docs/components.md), [sirsoft-basic](templates/sirsoft-basic/components.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -366,7 +366,7 @@ HtmlEditor는 내부적으로 **DOMPurify**를 사용하여 HTML을 정화합니
|
||||
## 관련 문서
|
||||
|
||||
- [컴포넌트 Props 레퍼런스](component-props.md) - Select, Input, Button Props
|
||||
- [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md) - Admin 컴포넌트 목록
|
||||
- [sirsoft-admin_basic 컴포넌트](../../templates/_bundled/sirsoft-admin_basic/docs/components.md) - Admin 컴포넌트 목록
|
||||
- [레이아웃 JSON](layout-json.md) - 레이아웃 JSON 스키마
|
||||
- [데이터 바인딩](data-binding.md) - {{}} 표현식, $t: 다국어
|
||||
- [상태 관리](state-management.md) - _local, setState
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
| `sirsoft-admin_basic` | 미선언 | 데스크톱 중심, Tailwind responsive 클래스는 동작 |
|
||||
| `sirsoft-basic` | `true` | MobileNav 포함, portable preset 지원 |
|
||||
|
||||
> 상세 컴포넌트 목록: [sirsoft-admin_basic](templates/sirsoft-admin_basic/components.md), [sirsoft-basic](templates/sirsoft-basic/components.md)
|
||||
> 상세 컴포넌트 목록: [sirsoft-admin_basic](../../templates/_bundled/sirsoft-admin_basic/docs/components.md), [sirsoft-basic](templates/sirsoft-basic/components.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -22,11 +22,13 @@
|
||||
<!-- AUTO-GENERATED-START: frontend-template-handlers -->
|
||||
| 템플릿 식별자 | 핸들러 문서 | TL;DR 핵심 |
|
||||
|--------------|-----------|----------|
|
||||
| `sirsoft-admin_basic` | [handlers.md](templates/sirsoft-admin_basic/handlers.md) | setLocale: 앱 언어 변경 (locale 파라미터) |
|
||||
| `sirsoft-basic` | [handlers.md](templates/sirsoft-basic/handlers.md) | setTheme/initTheme: 다크/라이트 모드 전환 (admin과 동일 키 공유) |
|
||||
|
||||
<!-- AUTO-GENERATED-END: frontend-template-handlers -->
|
||||
|
||||
> `sirsoft-admin_basic` 은 문서를 그 템플릿이 직접 소유합니다 — [templates/_bundled/sirsoft-admin_basic/docs/](../../templates/_bundled/sirsoft-admin_basic/docs/README.md).
|
||||
> 템플릿별 문서 위치는 [templates/README.md](templates/README.md) 를 따릅니다.
|
||||
|
||||
---
|
||||
|
||||
## 엔진 빌트인 핸들러
|
||||
@@ -55,5 +57,5 @@
|
||||
- [액션 핸들러 개요](actions-handlers.md)
|
||||
- [템플릿 개발 가이드](template-development.md)
|
||||
- [다크 모드 지원](dark-mode.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](../../templates/_bundled/sirsoft-admin_basic/docs/components.md)
|
||||
- [sirsoft-basic 컴포넌트](templates/sirsoft-basic/components.md)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# 템플릿별 컴포넌트·핸들러·레이아웃 문서
|
||||
|
||||
번들 템플릿의 컴포넌트/핸들러/레이아웃 상세 문서는 그 템플릿이 소유합니다(#601). 어느
|
||||
템플릿이 어디에 문서를 갖는지는 아래 표를 따릅니다 — 이관이 끝난 템플릿은 코어에 사본을
|
||||
남기지 않습니다.
|
||||
|
||||
| 템플릿 | 상태 | 문서 위치 |
|
||||
|---|---|---|
|
||||
| `sirsoft-admin_basic` | 이관 완료 | [templates/_bundled/sirsoft-admin_basic/docs/](../../../templates/_bundled/sirsoft-admin_basic/docs/README.md) |
|
||||
| `sirsoft-basic` | 이관 대기 (S3) | [components.md](sirsoft-basic/components.md) · [handlers.md](sirsoft-basic/handlers.md) · [layouts.md](sirsoft-basic/layouts.md) |
|
||||
|
||||
이관 배경·문서 체계 전반은 [extension-documentation.md](../../extension/extension-documentation.md)
|
||||
를 참고하세요.
|
||||
@@ -1,445 +0,0 @@
|
||||
# sirsoft-admin_basic 핸들러
|
||||
|
||||
> **템플릿 식별자**: `sirsoft-admin_basic` (type: admin)
|
||||
> **관련 문서**: [액션 핸들러 개요](../../actions-handlers.md) | [컴포넌트](./components.md) | [레이아웃](./layouts.md)
|
||||
|
||||
---
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. setLocale: 앱 언어 변경 (locale 파라미터)
|
||||
2. setTheme/initTheme: 다크/라이트 모드 전환 및 초기화
|
||||
3. scrollToSection: 특정 섹션으로 스크롤 이동 (offset 지원)
|
||||
4. initMenuFromUrl: URL 기반 메뉴 활성 상태 초기화
|
||||
5. filterVisibility 4종: 필터 패널 가시성 저장/토글/초기화
|
||||
6. multilingualTag 3종: 다국어 태그 저장/취소/업데이트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
|
||||
1. [setLocale](#setlocale)
|
||||
2. [setTheme / initTheme](#settheme--inittheme)
|
||||
3. [scrollToSection](#scrolltosection)
|
||||
4. [initMenuFromUrl](#initmenuefromurl)
|
||||
5. [필터 가시성 핸들러](#필터-가시성-핸들러)
|
||||
6. [다국어 태그 핸들러](#다국어-태그-핸들러)
|
||||
7. [핸들러 소스 파일 매핑](#핸들러-소스-파일-매핑)
|
||||
|
||||
---
|
||||
|
||||
## setLocale
|
||||
|
||||
앱 언어를 변경합니다. 번역 파일을 다시 로드하고 UI를 갱신합니다.
|
||||
|
||||
**소스**: `src/handlers/setLocaleHandler.ts`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setLocale",
|
||||
"params": {
|
||||
"locale": "en"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### params
|
||||
|
||||
| 필드 | 타입 | 필수 | 설명 |
|
||||
|------|------|------|------|
|
||||
| `locale` | string | ✅ | 변경할 로케일 코드 (예: `"ko"`, `"en"`, `"ja"`) |
|
||||
|
||||
### 동작
|
||||
|
||||
```text
|
||||
1. 로케일 변경 → 번역 파일 다시 로드
|
||||
2. _global.locale 자동 업데이트
|
||||
3. 모든 $t: 표현식 재평가
|
||||
```
|
||||
|
||||
### 사용 예시
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "lang_en_btn",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"text": "English"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setLocale",
|
||||
"params": {
|
||||
"locale": "en"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## setTheme / initTheme
|
||||
|
||||
### setTheme
|
||||
|
||||
다크/라이트 모드를 전환합니다.
|
||||
|
||||
**소스**: `src/handlers/setThemeHandler.ts`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setTheme",
|
||||
"params": {
|
||||
"theme": "dark"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### setTheme params
|
||||
|
||||
| 필드 | 타입 | 필수 | 설명 |
|
||||
|------|------|------|------|
|
||||
| `theme` | string | ✅ | `"light"`, `"dark"`, `"auto"` (시스템 설정 따름) |
|
||||
|
||||
### 동작
|
||||
|
||||
```text
|
||||
1. localStorage에 테마 설정 저장
|
||||
2. document.documentElement에 class 적용 (dark/light)
|
||||
3. Tailwind dark: variant 활성화/비활성화
|
||||
```
|
||||
|
||||
### initTheme
|
||||
|
||||
앱 시작 시 저장된 테마 설정을 적용합니다. 주로 `init_actions`에서 사용합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"init_actions": [
|
||||
{
|
||||
"handler": "initTheme"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
params 없이 호출합니다. localStorage에 저장된 테마 설정을 복원합니다.
|
||||
|
||||
### 사용 예시
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "theme_toggle",
|
||||
"type": "composite",
|
||||
"name": "ThemeToggle",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "setTheme",
|
||||
"params": {
|
||||
"theme": "{{_global.theme === 'dark' ? 'light' : 'dark'}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## scrollToSection
|
||||
|
||||
특정 섹션으로 스크롤합니다. 오프셋 지원에 특화되어 있습니다.
|
||||
|
||||
**소스**: `src/handlers/scrollToSectionHandler.ts`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "scrollToSection",
|
||||
"params": {
|
||||
"selector": "#features",
|
||||
"offset": -80
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### params
|
||||
|
||||
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|
||||
|------|------|------|--------|------|
|
||||
| `selector` | string | ✅ | - | CSS 선택자 (예: `"#section-id"`, `".class-name"`) |
|
||||
| `offset` | number | ❌ | `0` | 스크롤 오프셋 (음수: 위로, 양수: 아래로). 고정 헤더 높이 보상에 사용 |
|
||||
|
||||
### 동작
|
||||
|
||||
```text
|
||||
1. document.querySelector(selector)로 대상 요소 검색
|
||||
2. 요소의 위치 계산 + offset 적용
|
||||
3. window.scrollTo({ top, behavior: 'smooth' })로 부드러운 스크롤
|
||||
```
|
||||
|
||||
### 사용 예시
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "nav_features",
|
||||
"type": "basic",
|
||||
"name": "A",
|
||||
"props": {
|
||||
"text": "$t:common.features",
|
||||
"className": "cursor-pointer"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "scrollToSection",
|
||||
"params": {
|
||||
"selector": "#features",
|
||||
"offset": -80
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## initMenuFromUrl
|
||||
|
||||
현재 URL을 기반으로 사이드바/네비게이션 메뉴의 활성 상태를 초기화합니다. 주로 관리자 템플릿의 `init_actions`에서 사용합니다.
|
||||
|
||||
**소스**: `src/handlers/initMenuFromUrlHandler.ts`
|
||||
|
||||
```json
|
||||
{
|
||||
"init_actions": [
|
||||
{
|
||||
"handler": "initMenuFromUrl"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### params
|
||||
|
||||
없음. 현재 URL 경로를 메뉴 항목과 매칭하여 활성 메뉴를 자동 설정합니다.
|
||||
|
||||
### 동작
|
||||
|
||||
```text
|
||||
1. 현재 URL 경로 (window.location.pathname) 추출
|
||||
2. 사이드바 메뉴 데이터에서 URL 매칭
|
||||
3. 매칭된 메뉴 항목의 is_active 상태 설정
|
||||
4. 부모 메뉴도 자동으로 펼침 상태 설정
|
||||
```
|
||||
|
||||
### 사용 예시 (_admin_base.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"init_actions": [
|
||||
{ "handler": "initTheme" },
|
||||
{ "handler": "initMenuFromUrl" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 필터 가시성 핸들러
|
||||
|
||||
목록 화면에서 필터 패널의 가시성(표시/숨김)을 관리합니다. localStorage에 상태를 저장하여 새로고침 후에도 유지합니다.
|
||||
|
||||
**소스**: `src/handlers/filterVisibilityHandler.ts`
|
||||
|
||||
### initFilterVisibility
|
||||
|
||||
저장된 필터 가시성 상태를 `_local`에 복원합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"init_actions": [
|
||||
{
|
||||
"handler": "initFilterVisibility"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### saveFilterVisibility
|
||||
|
||||
현재 필터 가시성 상태를 localStorage에 저장합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "saveFilterVisibility",
|
||||
"params": {
|
||||
"filters": "{{_local.filterVisibility}}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### toggleFilterVisibility
|
||||
|
||||
특정 필터 키의 가시성을 토글합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "toggleFilterVisibility",
|
||||
"params": {
|
||||
"key": "advancedFilters"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### resetFilterVisibility
|
||||
|
||||
모든 필터 가시성을 초기 상태로 리셋합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "resetFilterVisibility"
|
||||
}
|
||||
```
|
||||
|
||||
### 핸들러 params 요약
|
||||
|
||||
| 핸들러 | params | 설명 |
|
||||
|--------|--------|------|
|
||||
| `initFilterVisibility` | 없음 | localStorage → `_local` 복원 |
|
||||
| `saveFilterVisibility` | `{ filters }` | `_local` → localStorage 저장 |
|
||||
| `toggleFilterVisibility` | `{ key }` | 특정 키 토글 |
|
||||
| `resetFilterVisibility` | 없음 | 전체 초기화 |
|
||||
|
||||
### 사용 예시 (목록 페이지)
|
||||
|
||||
```json
|
||||
{
|
||||
"init_actions": [
|
||||
{ "handler": "initFilterVisibility" }
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"id": "filter_toggle_btn",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"text": "$t:common.toggle_filters"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "toggleFilterVisibility",
|
||||
"params": { "key": "advancedFilters" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "filter_section",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.filterVisibility?.advancedFilters}}",
|
||||
"children": [
|
||||
{ "comment": "필터 컴포넌트들" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 다국어 태그 핸들러
|
||||
|
||||
다국어 입력 컴포넌트(MultilingualInput)에서 사용하는 태그 관리 핸들러입니다.
|
||||
|
||||
**소스**: `src/handlers/multilingualTagHandler.ts`
|
||||
|
||||
### saveMultilingualTag
|
||||
|
||||
다국어 태그를 저장합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "saveMultilingualTag",
|
||||
"params": {
|
||||
"field": "tags",
|
||||
"locale": "{{_global.locale}}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### cancelMultilingualTag
|
||||
|
||||
다국어 태그 편집을 취소합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "cancelMultilingualTag"
|
||||
}
|
||||
```
|
||||
|
||||
### updateMultilingualTagValue
|
||||
|
||||
다국어 태그 값을 업데이트합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "updateMultilingualTagValue",
|
||||
"params": {
|
||||
"field": "tags",
|
||||
"locale": "ko",
|
||||
"value": "{{$event.target.value}}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 태그 핸들러 params 요약
|
||||
|
||||
| 핸들러 | params | 설명 |
|
||||
|--------|--------|------|
|
||||
| `saveMultilingualTag` | `{ field, locale }` | 태그 저장 |
|
||||
| `cancelMultilingualTag` | 없음 | 편집 취소 |
|
||||
| `updateMultilingualTagValue` | `{ field, locale, value }` | 값 업데이트 |
|
||||
|
||||
---
|
||||
|
||||
## 핸들러 소스 파일 매핑
|
||||
|
||||
| 핸들러명 | 소스 파일 | 등록 함수 |
|
||||
|---------|----------|----------|
|
||||
| `setLocale` | `src/handlers/setLocaleHandler.ts` | `setLocaleHandler` |
|
||||
| `setTheme`, `initTheme` | `src/handlers/setThemeHandler.ts` | `initTheme` |
|
||||
| `scrollToSection` | `src/handlers/scrollToSectionHandler.ts` | `scrollToSectionHandler` |
|
||||
| `initMenuFromUrl` | `src/handlers/initMenuFromUrlHandler.ts` | `initMenuFromUrlHandler` |
|
||||
| `initFilterVisibility`, `saveFilterVisibility`, `toggleFilterVisibility`, `resetFilterVisibility` | `src/handlers/filterVisibilityHandler.ts` | `initFilterVisibilityHandler` |
|
||||
| `saveMultilingualTag`, `cancelMultilingualTag`, `updateMultilingualTagValue` | `src/handlers/multilingualTagHandler.ts` | `saveMultilingualTagHandler` |
|
||||
|
||||
---
|
||||
|
||||
## 주의사항
|
||||
|
||||
```text
|
||||
이 핸들러들은 sirsoft-admin_basic 템플릿에서만 등록됨 (다른 템플릿에서 미지원 가능)
|
||||
범용 핸들러(navigate, apiCall, setState 등)와 달리 템플릿 의존적
|
||||
✅ 커스텀 핸들러이므로 template.json의 핸들러 등록 확인 필요
|
||||
✅ 범용 핸들러는 actions-handlers.md 참조
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [액션 핸들러 개요](../../actions-handlers.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](./components.md)
|
||||
- [sirsoft-admin_basic 레이아웃](./layouts.md)
|
||||
- [sirsoft-basic 핸들러](../sirsoft-basic/handlers.md)
|
||||
@@ -1,368 +0,0 @@
|
||||
# sirsoft-admin_basic 레이아웃
|
||||
|
||||
> **템플릿 식별자**: `sirsoft-admin_basic` (type: admin)
|
||||
> **관련 문서**: [컴포넌트](./components.md) | [핸들러](./handlers.md) | [레이아웃 JSON 스키마](../../layout-json.md)
|
||||
|
||||
---
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 베이스: _admin_base.json (사이드바 + 헤더 + 콘텐츠 슬롯)
|
||||
2. 코어 페이지 17개: 대시보드, 사용자, 역할, 설정, 확장 관리 등
|
||||
3. Partial 70개+: 탭, 모달, 패널, 필터 등
|
||||
4. 에러 페이지 6개: 401, 403, 404, 500, 503, maintenance
|
||||
5. 패턴: 목록(DataGrid+Pagination), 상세(Card), 폼(Form+FormField), 설정(Tab)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
|
||||
1. [페이지 맵 (트리 구조)](#페이지-맵-트리-구조)
|
||||
2. [카테고리별 가이드](#카테고리별-가이드)
|
||||
- [목록 페이지 패턴](#목록-페이지-패턴)
|
||||
- [상세 페이지 패턴](#상세-페이지-패턴)
|
||||
- [폼 페이지 패턴](#폼-페이지-패턴)
|
||||
- [설정 페이지 패턴](#설정-페이지-패턴)
|
||||
- [확장 관리 패턴](#확장-관리-패턴)
|
||||
- [에러 페이지 패턴](#에러-페이지-패턴)
|
||||
|
||||
---
|
||||
|
||||
## 페이지 맵 (트리 구조)
|
||||
|
||||
```text
|
||||
_admin_base.json (베이스 레이아웃)
|
||||
│
|
||||
├── admin_dashboard.json (대시보드)
|
||||
├── admin_login.json (로그인 — _admin_base 미상속)
|
||||
│
|
||||
├── 사용자 관리
|
||||
│ ├── admin_user_list.json (목록)
|
||||
│ ├── admin_user_form.json (생성/수정)
|
||||
│ └── admin_user_detail.json (상세)
|
||||
│
|
||||
├── 역할 관리
|
||||
│ ├── admin_role_list.json (목록)
|
||||
│ │ └── partials/admin_role_list/_modal_delete.json
|
||||
│ └── admin_role_form.json (생성/수정)
|
||||
│
|
||||
├── 메뉴 관리
|
||||
│ └── admin_menu_list.json (3패널 레이아웃)
|
||||
│ └── partials/admin_menu_list/
|
||||
│ ├── _panel_menu_list.json (좌측: 메뉴 트리)
|
||||
│ ├── _panel_form.json (중앙: 편집 폼)
|
||||
│ ├── _panel_detail.json (중앙: 상세 보기)
|
||||
│ ├── _panel_view.json (우측: 미리보기)
|
||||
│ └── _modal_delete.json
|
||||
│
|
||||
├── 환경 설정
|
||||
│ └── admin_settings.json (탭 네비게이션)
|
||||
│ └── partials/admin_settings/
|
||||
│ ├── _tab_general.json (일반)
|
||||
│ ├── _tab_mail.json (메일 발송 SMTP 설정)
|
||||
│ ├── _tab_notification_definitions.json (알림 정의)
|
||||
│ ├── _tab_security.json (보안)
|
||||
│ ├── _tab_upload.json (업로드)
|
||||
│ ├── _tab_drivers.json (드라이버)
|
||||
│ ├── _tab_seo.json (SEO)
|
||||
│ ├── _tab_advanced.json (고급)
|
||||
│ ├── _tab_info.json (시스템 정보)
|
||||
│ ├── _modal_cache_delete.json
|
||||
│ ├── _modal_core_changelog.json
|
||||
│ ├── _modal_core_update_guide.json
|
||||
│ ├── _modal_core_update_result.json
|
||||
│ ├── _modal_notification_template_form.json
|
||||
│ ├── _modal_notification_template_preview.json
|
||||
│ └── _modal_password_confirm.json
|
||||
│
|
||||
├── 모듈 관리
|
||||
│ └── admin_module_list.json
|
||||
│ └── partials/admin_module_list/
|
||||
│ ├── _modal_detail.json
|
||||
│ ├── _modal_install.json
|
||||
│ ├── _modal_manual_install.json
|
||||
│ ├── _modal_uninstall.json
|
||||
│ ├── _modal_update.json
|
||||
│ ├── _modal_deactivate_warning.json
|
||||
│ ├── _modal_force_activate.json
|
||||
│ ├── _modal_force_deactivate.json
|
||||
│ ├── _modal_extension_license.json
|
||||
│ └── _modal_refresh_layouts.json
|
||||
│
|
||||
├── 플러그인 관리
|
||||
│ └── admin_plugin_list.json
|
||||
│ └── partials/admin_plugin_list/
|
||||
│ ├── (모듈과 동일 구조 — 10개 모달)
|
||||
│ └── ...
|
||||
│
|
||||
├── 템플릿 관리
|
||||
│ ├── admin_template_list.json
|
||||
│ │ └── partials/admin_template_list/
|
||||
│ │ ├── _tab_admin.json (Admin 템플릿 탭)
|
||||
│ │ ├── _tab_user.json (User 템플릿 탭)
|
||||
│ │ ├── _modal_detail.json
|
||||
│ │ ├── _modal_install.json
|
||||
│ │ ├── _modal_manual_install.json
|
||||
│ │ ├── _modal_uninstall.json
|
||||
│ │ ├── _modal_update.json
|
||||
│ │ ├── _modal_activate.json
|
||||
│ │ ├── _modal_deactivate.json
|
||||
│ │ ├── _modal_force_activate.json
|
||||
│ │ ├── _modal_extension_license.json
|
||||
│ │ └── _modal_refresh_layouts.json
|
||||
│ └── admin_template_layout_edit.json (레이아웃 편집기)
|
||||
│ └── partials/admin_template_layout_edit/_modal_version_history.json
|
||||
│
|
||||
├── 스케줄 관리
|
||||
│ └── admin_schedule_list.json
|
||||
│ └── partials/admin_schedule_list/
|
||||
│ ├── _tab_schedules.json
|
||||
│ ├── _modal_form.json
|
||||
│ ├── _modal_delete.json
|
||||
│ ├── _modal_duplicate.json
|
||||
│ ├── _modal_history.json
|
||||
│ └── _modal_run.json
|
||||
│
|
||||
├── 메일 발송 로그
|
||||
│ └── admin_mail_send_log_list.json
|
||||
│ └── partials/admin_mail_send_log_list/
|
||||
│ ├── _partial_datagrid.json
|
||||
│ └── _partial_filter.json
|
||||
│
|
||||
├── 공통 Partial
|
||||
│ ├── partials/_modal_changelog.json
|
||||
│ └── partials/_modal_license.json
|
||||
│
|
||||
├── 에러 페이지
|
||||
│ └── errors/
|
||||
│ ├── 401.json (인증 필요)
|
||||
│ ├── 403.json (접근 거부)
|
||||
│ ├── 404.json (페이지 없음)
|
||||
│ ├── 500.json (서버 오류)
|
||||
│ ├── 503.json (서비스 불가)
|
||||
│ └── maintenance.json (점검 중)
|
||||
│
|
||||
├── 오버라이드
|
||||
│ └── overrides/sirsoft-sample/index.json
|
||||
│
|
||||
└── 테스트
|
||||
└── template_partial_test.json
|
||||
└── partials/template_partial_test/
|
||||
├── _content_section.json
|
||||
├── _header_section.json
|
||||
└── _info_card.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 카테고리별 가이드
|
||||
|
||||
### 목록 페이지 패턴
|
||||
|
||||
**대표**: `admin_user_list.json`, `admin_role_list.json`
|
||||
|
||||
**구성**:
|
||||
```text
|
||||
extends: _admin_base
|
||||
slots.content:
|
||||
└── PageHeader (제목, 액션 버튼)
|
||||
└── FilterGroup (선택적)
|
||||
└── DataGrid (columns, data, pagination)
|
||||
└── Pagination
|
||||
```
|
||||
|
||||
**data_sources**:
|
||||
```json
|
||||
{
|
||||
"id": "users",
|
||||
"endpoint": "/api/admin/users",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"params": { "page": "{{_local.page ?? 1}}", "per_page": "{{_local.per_page ?? 15}}" }
|
||||
}
|
||||
```
|
||||
|
||||
**핸들러 패턴**:
|
||||
- `apiCall` — 삭제, 상태 변경
|
||||
- `navigate` — 상세/수정 페이지 이동
|
||||
- `setState` — 필터, 페이지네이션 상태
|
||||
|
||||
**Partial 구조**: 모달 (삭제 확인, 상세 보기 등)
|
||||
|
||||
---
|
||||
|
||||
### 상세 페이지 패턴
|
||||
|
||||
**대표**: `admin_user_detail.json`
|
||||
|
||||
**구성**:
|
||||
```text
|
||||
extends: _admin_base
|
||||
data_sources: [상세 API (route.id 기반)]
|
||||
slots.content:
|
||||
└── PageHeader
|
||||
└── Card (기본 정보 섹션)
|
||||
└── Card (활동 내역 섹션)
|
||||
└── Card (권한 정보 섹션)
|
||||
```
|
||||
|
||||
**data_sources**:
|
||||
```json
|
||||
{
|
||||
"id": "user",
|
||||
"endpoint": "/api/admin/users/{{route.id}}",
|
||||
"method": "GET",
|
||||
"auto_fetch": true
|
||||
}
|
||||
```
|
||||
|
||||
**핸들러 패턴**:
|
||||
- `apiCall` — 상태 변경 (활성화/비활성화, 역할 변경)
|
||||
- `navigate` — 목록으로 이동, 수정 페이지 이동
|
||||
|
||||
---
|
||||
|
||||
### 폼 페이지 패턴
|
||||
|
||||
**대표**: `admin_user_form.json`, `admin_role_form.json`
|
||||
|
||||
**구성**:
|
||||
```text
|
||||
extends: _admin_base
|
||||
data_sources: [상세 API (수정 시), 참조 데이터 (Select 옵션)]
|
||||
slots.content:
|
||||
└── PageHeader
|
||||
└── Form
|
||||
├── FormField + Input (텍스트)
|
||||
├── FormField + Select (선택)
|
||||
├── FormField + Toggle (토글)
|
||||
└── Button (저장/취소)
|
||||
```
|
||||
|
||||
**핸들러 패턴**:
|
||||
- `apiCall` — 생성 (POST) / 수정 (PUT)
|
||||
- `navigate` — 성공 후 목록/상세로 이동
|
||||
|
||||
**주의사항**:
|
||||
```text
|
||||
✅ Form 내 Button에 type="button" 명시 (submit 방지)
|
||||
✅ 수정 폼은 route.id 존재 여부로 생성/수정 구분
|
||||
✅ FormField에 error prop으로 서버 검증 에러 표시
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 설정 페이지 패턴
|
||||
|
||||
**대표**: `admin_settings.json`
|
||||
|
||||
**구성**:
|
||||
```text
|
||||
extends: _admin_base
|
||||
data_sources: [설정 API]
|
||||
slots.content:
|
||||
└── PageHeader
|
||||
└── TabNavigation (tabs)
|
||||
└── Div (탭별 partial 조건부 렌더링)
|
||||
├── if: activeTab === 'general' → partial: _tab_general.json
|
||||
├── if: activeTab === 'mail' → partial: _tab_mail.json
|
||||
├── if: activeTab === 'security' → partial: _tab_security.json
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Partial 구조**: `partials/admin_settings/_tab_*.json` (9개 탭)
|
||||
|
||||
**핸들러 패턴**:
|
||||
- `setState` — 탭 전환
|
||||
- `apiCall` — 설정 저장
|
||||
- `openModal` — 확인 다이얼로그
|
||||
|
||||
---
|
||||
|
||||
### 확장 관리 패턴
|
||||
|
||||
**대표**: `admin_module_list.json`, `admin_plugin_list.json`, `admin_template_list.json`
|
||||
|
||||
**구성**:
|
||||
```text
|
||||
extends: _admin_base
|
||||
data_sources: [확장 목록 API]
|
||||
slots.content:
|
||||
└── PageHeader (새로고침, 수동 설치 버튼)
|
||||
└── DataGrid/CardGrid (확장 목록)
|
||||
├── StatusBadge (상태)
|
||||
├── ActionMenu (설치/활성화/비활성화/삭제/업데이트)
|
||||
└── ExtensionBadge (모듈 식별)
|
||||
modals:
|
||||
├── _modal_detail.json (상세 정보)
|
||||
├── _modal_install.json (설치 확인)
|
||||
├── _modal_uninstall.json (삭제 확인)
|
||||
├── _modal_update.json (업데이트)
|
||||
└── _modal_force_activate.json 등
|
||||
```
|
||||
|
||||
**핸들러 패턴**:
|
||||
- `apiCall` — 설치, 활성화, 비활성화, 삭제, 업데이트
|
||||
- `openModal` — 확인 다이얼로그
|
||||
- `setState` — 선택된 확장 정보 저장
|
||||
|
||||
**특수사항**:
|
||||
- 템플릿 관리는 Admin/User 탭 분리 (`_tab_admin.json`, `_tab_user.json`)
|
||||
- 레이아웃 편집기 (`admin_template_layout_edit.json`)는 CodeEditor + 실시간 미리보기
|
||||
|
||||
---
|
||||
|
||||
### 에러 페이지 패턴
|
||||
|
||||
**대표**: `errors/404.json`
|
||||
|
||||
**구성**:
|
||||
```text
|
||||
(extends 없음 — 독립 레이아웃)
|
||||
components:
|
||||
└── Div (전체 화면 중앙 정렬)
|
||||
├── Icon (에러 아이콘)
|
||||
├── H1 (에러 코드)
|
||||
├── P (에러 메시지)
|
||||
└── Button (홈으로 이동)
|
||||
```
|
||||
|
||||
**핸들러 패턴**:
|
||||
- `navigate` — 대시보드/홈으로 이동
|
||||
|
||||
---
|
||||
|
||||
## 베이스 레이아웃 구조
|
||||
|
||||
### _admin_base.json
|
||||
|
||||
모든 관리자 페이지의 공통 구조를 정의합니다.
|
||||
|
||||
```text
|
||||
_admin_base.json
|
||||
├── init_actions: [initTheme, initMenuFromUrl]
|
||||
├── data_sources: [admin_menu, notifications]
|
||||
├── components:
|
||||
│ ├── AdminSidebar (menu: admin_menu.data)
|
||||
│ ├── AdminHeader (user, notifications)
|
||||
│ ├── Toast
|
||||
│ ├── PageTransitionIndicator
|
||||
│ └── Div (content area)
|
||||
│ └── slot: "content" (← 하위 레이아웃이 채움)
|
||||
└── AdminFooter
|
||||
```
|
||||
|
||||
**슬롯**:
|
||||
- `content` — 각 페이지의 메인 콘텐츠가 삽입되는 위치
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [sirsoft-admin_basic 컴포넌트](./components.md)
|
||||
- [sirsoft-admin_basic 핸들러](./handlers.md)
|
||||
- [레이아웃 JSON 스키마](../../layout-json.md)
|
||||
- [레이아웃 상속](../../layout-json-inheritance.md)
|
||||
- [sirsoft-basic 레이아웃](../sirsoft-basic/layouts.md)
|
||||
@@ -242,6 +242,6 @@ AdminSidebar, AdminHeader, AdminFooter, PageHeader, DataGrid, CodeEditor, Dynami
|
||||
|
||||
- [sirsoft-basic 핸들러](./handlers.md)
|
||||
- [sirsoft-basic 레이아웃](./layouts.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](../sirsoft-admin_basic/components.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](../../../../templates/_bundled/sirsoft-admin_basic/docs/components.md)
|
||||
- [컴포넌트 개발 규칙](../../components.md)
|
||||
- [컴포넌트 Props 레퍼런스](../../component-props.md)
|
||||
|
||||
@@ -428,4 +428,4 @@ setLocale은 엔진 레벨(ActionDispatcher) 빌트인 — 별도 등록 불필
|
||||
- [액션 핸들러 개요](../../actions-handlers.md)
|
||||
- [sirsoft-basic 컴포넌트](./components.md)
|
||||
- [sirsoft-basic 레이아웃](./layouts.md)
|
||||
- [sirsoft-admin_basic 핸들러](../sirsoft-admin_basic/handlers.md)
|
||||
- [sirsoft-admin_basic 핸들러](../../../../templates/_bundled/sirsoft-admin_basic/docs/handlers.md)
|
||||
|
||||
@@ -642,6 +642,6 @@ _user_base.json
|
||||
|
||||
- [sirsoft-basic 컴포넌트](./components.md)
|
||||
- [sirsoft-basic 핸들러](./handlers.md)
|
||||
- [sirsoft-admin_basic 레이아웃](../sirsoft-admin_basic/layouts.md)
|
||||
- [sirsoft-admin_basic 레이아웃](../../../../templates/_bundled/sirsoft-admin_basic/docs/layouts.md)
|
||||
- [레이아웃 JSON 스키마](../../layout-json.md)
|
||||
- [레이아웃 상속](../../layout-json-inheritance.md)
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-08-31
|
||||
|
||||
### Added
|
||||
|
||||
- 동의 이력의 출처 "会員退会"(회원탈퇴) 일본어 라벨 추가
|
||||
|
||||
## [1.0.1] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
@@ -203,6 +203,7 @@ return [
|
||||
'register' => '会員登録',
|
||||
'mypage' => 'マイページ',
|
||||
'mypage_renew_all' => 'マイページ一括再同意',
|
||||
'withdraw' => '会員退会',
|
||||
],
|
||||
'col' => [
|
||||
'created_at' => '時点',
|
||||
|
||||
@@ -238,7 +238,8 @@
|
||||
"preference_center": "環境設定",
|
||||
"mypage": "マイページ",
|
||||
"register": "会員登録",
|
||||
"mypage_renew_all": "マイページ一括再同意"
|
||||
"mypage_renew_all": "マイページ一括再同意",
|
||||
"withdraw": "会員退会"
|
||||
},
|
||||
"col": {
|
||||
"created_at": "時点",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 plugin (sirsoft-gdpr) Japanese language pack (bundled)",
|
||||
"ja": "G7 プラグイン (sirsoft-gdpr) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"scope": "plugin",
|
||||
"target_identifier": "sirsoft-gdpr",
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# 게시판 — 에이전트 가이드
|
||||
|
||||
> 이 문서는 이 모듈을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 [README.md](README.md) 를 보세요.
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 유형: 모듈 (sirsoft-board) — 게시판·게시글·댓글·신고·게시판별 알림설정 도메인. 관리자 CRUD + 공개 API 만 소유하고, 방문자 화면(목록/상세/글쓰기)은 템플릿이 그린다
|
||||
2. 확장 방식: 발행 훅 90개(전량 action/filter) — 새 콘텐츠 타입 연동은 `EcommerceInquiryHookListener` 식 필터 훅 위임 패턴을 참고
|
||||
3. 건드리면 안 되는 것: 비밀글 게이팅(`SecretContentGate`)을 우회하는 신규 조회 경로, `chunk()`(OFFSET) 로 삭제·갱신 순회, count 컬럼(posts_count 등) 직접 갱신
|
||||
4. 작업 위치: `modules/_bundled/sirsoft-board` — 활성 디렉토리 직접 수정 금지
|
||||
5. 반영: `php artisan module:update sirsoft-board --force`
|
||||
```
|
||||
|
||||
## 1. 이 확장은 무엇인가
|
||||
|
||||
<!-- @intent START -->
|
||||
게시판·게시글·댓글·신고·게시판별 알림설정을 소유하는 콘텐츠 도메인 모듈입니다. 운영자가
|
||||
`/admin/boards`에서 게시판을 자유롭게 생성(게시판 유형 `basic`/`gallery`/`card` 선택, 비밀글·
|
||||
답변형·트리거 기반 관리자 알림 등 게시판별 세부 설정)하면, 그 게시판마다 동적 권한
|
||||
(`sirsoft-board.{slug}.*`)·역할(`{slug}.manager`/`{slug}.step`)·메뉴가 자동 생성됩니다 —
|
||||
게시판 하나하나가 사실상 독립된 작은 확장처럼 자기 권한 체계를 갖습니다.
|
||||
|
||||
**소유 범위는 관리자 CRUD + 공개 API 까지입니다.** 방문자가 실제로 보는 목록/상세/글쓰기
|
||||
화면은 이 모듈이 그리지 않습니다 — `resources/layouts/` 46개가 전부 `admin` 그룹인 것이
|
||||
그 증거입니다. 공개 조회·작성은 `routes/api.php` 의 `boards.*` 라우트(비회원도 접근하는
|
||||
`optional.sanctum`)로만 나가고, 그 API 를 소비해 실제 화면을 그리는 것은 템플릿(`sirsoft-basic`)
|
||||
쪽 책임입니다. 그래서 이 모듈에 의존하는 확장은 지금 템플릿 하나뿐입니다 — 새 방문자 화면이
|
||||
필요하면 이 모듈이 아니라 그 화면을 쓰는 템플릿/모듈 쪽에 레이아웃을 추가합니다.
|
||||
|
||||
**설계 원칙**: 게시판은 이커머스 문의·후기처럼 "게시판을 흉내 낸" 다른 도메인의 콘텐츠 저장소로도
|
||||
쓰입니다. 그래서 이 모듈을 다른 도메인이 재사용하는 지점은 코드 결합이 아니라 **필터 훅
|
||||
위임**(`EcommerceInquiryHookListener`)입니다 — 이커머스 모듈이 자기 문의 게시판을 만들 때
|
||||
`sirsoft-board` 를 직접 `use` 하지 않고, board 쪽 흐름 중간에서 자기 로직으로 갈아끼웁니다.
|
||||
|
||||
**의도적으로 하지 않는 것**: 게시판 콘텐츠에 대한 실시간 브로드캐스트(WebSocket)는 제공하지
|
||||
않습니다 — 알림은 전부 코어 `GenericNotification`(mail/database) 경유이며, 새로고침 없이
|
||||
갱신되는 목록 같은 기능은 이 모듈의 범위 밖입니다. 또한 비밀글 판정은 이 모듈이 API 계층에서
|
||||
전량 서버측으로 강제합니다(`SecretContentGate`, KVE-2026-1914) — 클라이언트가 비밀글 여부를
|
||||
판단해 화면만 가리는 방식은 쓰지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 2. 디렉토리 지도
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `module.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `module.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Http/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Http/Resources/` | API 리소스 | 목록 응답은 화면이 실제로 그리는 것만 싣는다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Repositories/` | 데이터 접근 | 목록 쿼리는 컬럼 프루닝·정렬 화이트리스트 확인 |
|
||||
| `src/Models/` | Eloquent 모델 | 스키마 변경 시 마이그레이션 + 업그레이드 스텝 동반 |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/Enums/` | 상태·타입·분류 | 문자열 리터럴 대신 Enum 을 SSoT 로 둔다 |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `src/lang/` | 백엔드 다국어 | ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
| `database/migrations/` | 마이그레이션 | 한국어 comment + `down()` 필수, 기설치본은 업그레이드 스텝으로 백필 |
|
||||
| `database/seeders/` | 시더 | composer autoload 등록 + `extension:update-autoload` |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan module:update sirsoft-board --force` (빌드 불필요) |
|
||||
| `resources/routes/` | 라우트 → 레이아웃 매핑 (분할) | `php artisan module:update sirsoft-board --force` |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan module:build` → `php artisan module:update sirsoft-board --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan module:update sirsoft-board --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan module:update sirsoft-board --force` |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
<!-- @generated:directory-map END -->
|
||||
|
||||
## 3. 핵심 흐름
|
||||
|
||||
<!-- @intent START -->
|
||||
**게시판 생성**: `Admin\BoardController` → `StoreBoardRequest`(다국어 이름·slug 유일성·게시판
|
||||
유형 검증) → `BoardService::createBoard()` → `BoardRepository` 로 `boards` 행 생성 후, 같은
|
||||
트랜잭션 안에서 `BoardPermissionService`(동적 권한 3계층 생성) → 역할(manager/step) 생성 →
|
||||
관리자 메뉴 등록까지 연쇄 실행됩니다. 이 연쇄 때문에 `getDynamicPermissionIdentifiers()` /
|
||||
`getDynamicRoleIdentifiers()` / `getDynamicMenuSlugs()` 가 `boards` 테이블을 다시 조회해
|
||||
전수를 재구성합니다 — 게시판 삭제·정리(clean-up) 시 "stale 판정"의 기준이 이 세 메서드입니다.
|
||||
|
||||
**게시글 작성 → 비밀글 게이팅**: `User\PostController` → `StorePostRequest`
|
||||
(`sirsoft-board.post.store_validation_rules` 필터로 게시판별 커스텀 규칙 추가) →
|
||||
`PostService::createPost()` (`before_create`→`filter_create_data`→`after_create` 훅 순서,
|
||||
`sirsoft-board.post.user_create` IDV 정책 게이트 통과 후) → `PostRepository`. 조회 시에는
|
||||
`PostResource` 가 `SecretContentGate` 로 비밀글 여부·열람 권한을 판정해 `content`/`title`/
|
||||
`reply`/`attachments` 를 마스킹합니다 — 이 판정은 댓글 목록·첨부 다운로드에도 **개별
|
||||
재적용**됩니다(부모 글에서 한 번 판정하고 끝나지 않음, KVE-2026-1914).
|
||||
|
||||
**신고 접수 → 처리**: `User\ReportController` → `StoreReportRequest` → `ReportService::create()`
|
||||
(`before_create`→`filter_create_data`→`after_create`) → 신고 접수 관리자 알림 발송. 관리자가
|
||||
`Admin\ReportController` 에서 처리(블라인드/삭제/복원)하면 `ReportService` 가 대상 게시글/댓글
|
||||
서비스(`PostService`/`CommentService`)를 호출해 실제 콘텐츠 상태를 바꾸고, 그 결과가
|
||||
`report_action`/`post_action` 알림으로 원 작성자에게 통지됩니다. 신고 삭제·일괄 처리는
|
||||
관리자 민감 작업 IDV 정책(`report.delete`/`report.bulk_action`)이 걸려 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 4. 확장점
|
||||
|
||||
<!-- @generated:extension-points-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 확장점 | 수 | 상세 |
|
||||
|---|---|---|
|
||||
| 발행 훅 | 90개 | [발행 훅](docs/extension-points.md#발행-훅) |
|
||||
| 구독 훅 | 77개 | [구독 훅](docs/extension-points.md#구독-훅) |
|
||||
| 훅 리스너 | 16개 | [훅 리스너](docs/extension-points.md#훅-리스너) |
|
||||
| 레이아웃 확장 | 5개 | [레이아웃 확장](docs/extension-points.md#레이아웃-확장) |
|
||||
| 미들웨어 | 0개 | [미들웨어](docs/extension-points.md#미들웨어) |
|
||||
| 브로드캐스트 채널 | 0개 | [브로드캐스트 채널](docs/extension-points.md#브로드캐스트-채널) |
|
||||
| 스케줄 | 2개 | [스케줄](docs/extension-points.md#스케줄) |
|
||||
| 알림 정의 | 7개 | [알림 정의](docs/extension-points.md#알림-정의) |
|
||||
<!-- @generated:extension-points-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
발행 훅 90개는 전부 `action`/`filter` 이며 코어 브로드캐스트 채널은 쓰지 않습니다(구독 훅
|
||||
목록의 이커머스 8개가 예로 보여주듯, **콘텐츠를 다른 도메인이 재사용하는 자리는 훅이지 상속이
|
||||
아닙니다**). 새 게시판 세부 검증 규칙을 추가하려면 `board.store_validation_rules`/
|
||||
`update_validation_rules` filter 를, 게시판 생성 후 부가 리소스를 함께 만들려면
|
||||
`board.after_create` action 을 잡습니다. 게시글/댓글의 `before_*`→`filter_*_data`→`after_*`
|
||||
3단 패턴은 전 도메인(board/comment/report/attachment)에 동일하게 반복되므로, 하나를 배우면
|
||||
나머지에 그대로 적용됩니다. 훅 리스너 16개 중 `EcommerceInquiryHookListener` 는 이 모듈의
|
||||
CRUD 흐름 자체를 **자기 도메인으로 대체**하는 가장 무거운 형태의 확장 사례입니다 — 새로운
|
||||
"게시판을 흉내 낸 도메인"을 만들 때 참고할 선례입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 5. 수정 시 동반 의무
|
||||
|
||||
- [ ] `_bundled` 에서만 수정하고 `php artisan module:update sirsoft-board --force` 로 반영
|
||||
- [ ] manifest version 상향 시 `package.json` · `package-lock.json` · `composer.json` 동기화 + CHANGELOG 기재
|
||||
- [ ] 스키마 변경 시 마이그레이션(한국어 comment + `down()`) + 기설치본 백필용 업그레이드 스텝
|
||||
- [ ] 발행 훅 추가·이름 변경 시 `php artisan ext:docgen` 재실행 (구독하는 확장의 계약이 바뀝니다)
|
||||
- [ ] API 표면 변경 시 `php artisan api:docgen --scope=module:sirsoft-board` 재실행 + `docs/api/**` 갱신
|
||||
- [ ] 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
|
||||
- [ ] 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
|
||||
- [ ] 비밀글이 관여하는 새 조회 경로(댓글·첨부·검색 결과 등)를 추가할 때 `SecretContentGate` 재적용 (KVE-2026-1914 — 부모에서 한 번 판정하고 끝나지 않는다)
|
||||
- [ ] `boards`/`board_posts`/`board_comments` 의 count 컬럼(`posts_count`/`comments_count`/`replies_count`/`attachments_count`)은 훅 리스너(`*CountSyncListener`)가 갱신 — Service 에서 직접 증감 금지
|
||||
- [ ] 게시판 삭제 시 `getDynamicPermissionIdentifiers()`/`getDynamicRoleIdentifiers()`/`getDynamicMenuSlugs()` 가 최신 상태를 반영하도록 동일 트랜잭션에서 정리 (module.php `uninstall()` 의 `chunkById` 패턴 참고 — OFFSET 순회로 삭제 금지)
|
||||
|
||||
## 6. 금지 패턴
|
||||
|
||||
<!-- @intent START -->
|
||||
| 금지 | 올바른 사용 | 이유 |
|
||||
|---|---|---|
|
||||
| 비밀글 상세만 `SecretContentGate` 로 막고 댓글 목록·첨부 다운로드는 그대로 노출 | 댓글 목록은 부모 글 비밀 여부 확인 후 빈 배열, 첨부 서빙은 403 — 두 경로 모두 게이트 재적용 | 상세 API 하나만 막으면 같은 정보가 형제 엔드포인트로 새어나간다 (KVE-2026-1914) |
|
||||
| 게시글/댓글 삭제·복원 시 `posts_count`/`comments_count` 를 Service 에서 `increment()`/`decrement()` | 훅(`after_create`/`after_delete`/`after_restore`) 리스너의 count 동기화에 맡긴다 | 직접 증감은 훅 기반 동기화와 이중 집계되어 카운트가 어긋난다 |
|
||||
| 게시판별 동적 권한/역할을 board 삭제와 별도 시점에 정리 | `BoardService` 삭제 흐름 안에서 즉시 정리(또는 stale cleanup 이 `getDynamicPermissionIdentifiers()` 로 정확히 판정하게 유지) | 정리가 늦으면 존재하지 않는 게시판의 권한이 역할에 남아 관리 화면에 유령 항목이 뜬다 |
|
||||
| 새 콘텐츠 타입을 board 코드에 `if ($type === 'inquiry')` 로 직접 분기 | `EcommerceInquiryHookListener` 처럼 필터 훅으로 CRUD 를 위임 | board 코드가 알지 못하는 도메인이 늘어날수록 분기가 무한 증식한다 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 7. 테스트 실행
|
||||
|
||||
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 개수 | 위치 |
|
||||
|---|---|---|
|
||||
| PHPUnit | 155개 | `modules/_bundled/sirsoft-board/tests` |
|
||||
| Vitest | 33개 | `vitest.config.ts` |
|
||||
| Playwright | 26개 | `tests/Playwright` |
|
||||
| 시나리오 매니페스트 | 34개 | `tests/scenarios` |
|
||||
|
||||
기저 TestCase: `tests/ModuleTestCase.php` — 확장 테스트는 이 클래스를 상속합니다 (`Tests\TestCase` 직접 상속 금지).
|
||||
|
||||
```bash
|
||||
# PHPUnit (변경 범위만) (Bash)
|
||||
php vendor/bin/phpunit modules/_bundled/sirsoft-board/tests --filter='<대상클래스>'
|
||||
|
||||
# Vitest (확장 디렉토리에서) (PowerShell)
|
||||
cd modules/_bundled/sirsoft-board && powershell -Command "npm run test:run -- <대상>"
|
||||
|
||||
# Playwright E2E (Bash)
|
||||
npx playwright test modules/_bundled/sirsoft-board/tests/Playwright/specs/<대상>.spec.ts
|
||||
|
||||
```
|
||||
|
||||
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
|
||||
<!-- @generated:test-commands END -->
|
||||
|
||||
## 8. 문서 목차
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
@@ -0,0 +1,171 @@
|
||||
# 게시판
|
||||
|
||||
**G7 모듈 · sirsoft-board**
|
||||
게시판 관리를 위한 모듈
|
||||
|
||||
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-1.1.0-0066FF?style=flat-square" alt="version 1.1.0">
|
||||
<img src="https://img.shields.io/badge/type-%EB%AA%A8%EB%93%88-555555?style=flat-square" alt="type 모듈">
|
||||
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 >=7.0.10">
|
||||
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
|
||||
</p>
|
||||
<!-- @generated:badges END -->
|
||||
|
||||
---
|
||||
|
||||
[소개](#소개) · [주요 기능](#주요-기능) · [동작 방식](#동작-방식) · [요구 사항](#요구-사항) · [설치](#설치) · [관리자 설정](#관리자-설정) · [사용 방법](#사용-방법) · [다른 확장과의 연동](#다른-확장과의-연동) · [문서](#문서) · [트러블슈팅](#트러블슈팅) · [변경 이력](#변경-이력) · [라이선스](#라이선스)
|
||||
|
||||
---
|
||||
|
||||
## 소개
|
||||
|
||||
<!-- @intent START -->
|
||||
게시판·게시글·댓글·신고를 관리하는 콘텐츠 모듈입니다. 운영자가 관리자 화면에서 자유형(가로형/
|
||||
갤러리형/카드형) 게시판을 원하는 개수만큼 만들고, 게시판마다 비밀글·답변형·본인인증·자동 알림
|
||||
같은 세부 정책을 독립적으로 설정할 수 있습니다.
|
||||
|
||||
이 모듈은 관리자 화면과 공개 API 만 제공합니다. 방문자가 보는 목록·상세·글쓰기 화면은
|
||||
템플릿(`sirsoft-basic`)이 이 모듈의 API 를 호출해 그립니다 — 운영자 입장에서는 "게시판 콘텐츠는
|
||||
여기서 관리하고, 화면 디자인은 템플릿이 담당한다"로 이해하면 됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 주요 기능
|
||||
|
||||
<!-- @intent START -->
|
||||
| 영역 | 설명 |
|
||||
|---|---|
|
||||
| 게시판 관리 | 게시판 생성/수정/삭제, 게시판 유형(기본/갤러리/카드) 선택, 게시판별 세부 설정 일괄 적용 |
|
||||
| 게시글·댓글 | 작성/수정/삭제/블라인드/복원, 답변형 게시판(원글-답변 트리), 대댓글, 비밀글 |
|
||||
| 신고 처리 | 사용자 신고 접수 → 관리자 검토 → 블라인드/삭제/복원 처리, 처리 결과 알림 |
|
||||
| 첨부파일 | 업로드/다운로드/순서 변경, 게시판별 허용 확장자·용량 제한 |
|
||||
| 대시보드 | 게시판별 게시글·댓글·신고 현황과 추세, 미처리 신고 요약 |
|
||||
| 알림 | 새 댓글/대댓글/답변글/신고 접수/처리 결과를 메일·앱 내 알림으로 발송, 회원별 수신 여부 설정 |
|
||||
| 본인인증 연동 | 게시글/댓글 삭제, 신고 작성, 첫 글 작성 등 민감 작업에 코어 IDV 정책 적용(기본은 비활성) |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 동작 방식
|
||||
|
||||
<!-- @intent START -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
V[방문자] -->|공개 API 호출| T[템플릿 화면]
|
||||
T -->|GET boards/posts| API[게시판 공개 API]
|
||||
API --> SVC[PostService/CommentService]
|
||||
SVC --> DB[(board_posts 등)]
|
||||
|
||||
A[운영자] -->|관리자 화면| ADM[게시판 관리 UI]
|
||||
ADM -->|CRUD·블라인드·신고 처리| SVC
|
||||
SVC -->|훅 발행| N[알림/집계/검색색인]
|
||||
```
|
||||
|
||||
방문자는 템플릿이 그린 화면에서 공개 API 만 호출하고, 운영자는 이 모듈이 직접 제공하는
|
||||
관리자 화면을 씁니다. 두 경로 모두 결국 같은 Service 계층을 거치므로 비밀글 게이팅·카운트
|
||||
동기화·훅 발행은 어느 쪽에서 들어오든 동일하게 적용됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 요구 사항
|
||||
|
||||
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| G7 코어 | `>=7.0.10` |
|
||||
| PHP | `^8.2` |
|
||||
<!-- @generated:requirements END -->
|
||||
|
||||
## 설치
|
||||
|
||||
<!-- @generated:install START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
```bash
|
||||
# 번들 설치 (코어에 동봉된 소스에서 설치)
|
||||
php artisan module:install sirsoft-board
|
||||
|
||||
# 활성화
|
||||
php artisan module:activate sirsoft-board
|
||||
|
||||
# 업데이트 (번들 소스 기준 강제 반영)
|
||||
php artisan module:update sirsoft-board --force
|
||||
```
|
||||
|
||||
저장소: https://github.com/gnuboard/g7-module-sirsoft-board
|
||||
<!-- @generated:install END -->
|
||||
|
||||
## 관리자 설정
|
||||
|
||||
<!-- @generated:settings-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_별도의 관리자 설정 항목이 없습니다._
|
||||
<!-- @generated:settings-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
위 표가 비어 있는 이유는 이 모듈에 전역 환경설정(`getSettingsSchema()`) 이 없기 때문입니다 —
|
||||
설정은 전역이 아니라 **게시판 하나하나**에 딸려 있습니다(`/admin/boards/{slug}/settings`).
|
||||
게시판을 만들 때 기본/갤러리/카드 유형을 고르면 그 유형의 기본값이 채워지고, 이후 기본
|
||||
정보·목록 표시·게시글 정책·댓글 정책·첨부 정책·본인인증·알림·SEO 탭에서 게시판별로 따로
|
||||
조정합니다. 여러 게시판에 같은 값을 한 번에 반영하려면 게시판 목록 화면의 "설정 일괄 적용"을
|
||||
씁니다(`settings.before_bulk_apply`/`after_bulk_apply` 훅으로 계측 가능).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 사용 방법
|
||||
|
||||
<!-- @intent START -->
|
||||
**게시판 신설**: `/admin/boards` → "게시판 추가" → 이름·slug·유형 지정 → 저장. 저장 즉시
|
||||
관리자 메뉴·동적 권한(`sirsoft-board.{slug}.*`)·역할(`{slug}.manager`)이 자동 생성되므로,
|
||||
바로 이어서 "권한" 탭에서 그 게시판을 담당할 운영자에게 `{slug}.manager` 역할을 부여합니다.
|
||||
|
||||
**신고 처리**: 방문자가 게시글/댓글을 신고하면 `/admin/boards/reports` 에 접수되고 담당자에게
|
||||
메일이 갑니다. 신고 상세에서 신고 사유·신고 이력을 확인한 뒤 블라인드/삭제/복원 중 하나로
|
||||
처리하면, 그 결과가 원 작성자에게 자동으로 통지됩니다 — 별도로 작성자에게 안내 메일을 보낼
|
||||
필요가 없습니다.
|
||||
|
||||
**여러 게시판 설정 일괄 변경**: 예를 들어 전체 게시판의 첨부 용량 상한을 한 번에 올리고 싶으면,
|
||||
게시판 목록에서 대상 게시판을 체크한 뒤 "설정 일괄 적용" 모달에서 첨부 탭 값만 바꿔 적용합니다.
|
||||
다른 탭 값은 그대로 유지되고, 체크한 게시판에만 반영됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 다른 확장과의 연동
|
||||
|
||||
<!-- @generated:integrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
없음 — 코어만으로 동작합니다.
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
| 확장 | 유형 | 요구 버전 |
|
||||
|---|---|---|
|
||||
| `sirsoft-basic` | 템플릿 | `>=1.0.0` |
|
||||
<!-- @generated:integrations END -->
|
||||
|
||||
## 문서
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
<!-- @intent START -->
|
||||
| 증상 | 원인 | 조치 |
|
||||
|---|---|---|
|
||||
| 게시판 삭제 후에도 관리자 화면에 그 게시판 권한/역할이 남아 있음 | 정리 배치가 아직 실행되지 않았거나 삭제 트랜잭션이 중간에 실패 | `getDynamicPermissionIdentifiers()`/`getDynamicRoleIdentifiers()` 는 현재 `boards` 테이블 기준으로 계산되므로, 확장 정리 커맨드를 다시 실행하면 stale 항목이 잡힙니다 |
|
||||
| 검색어에 `+`, `-`, `"` 를 넣으면 결과가 0건으로 나옴 | 코어 검색 정제기가 FULLTEXT 연산자를 제거한 뒤 검색 — 연산자만 입력하면 빈 결과가 정상 동작 | 오류가 아닙니다. 실제 키워드를 함께 입력하면 정상 매칭됩니다 |
|
||||
| 비밀글의 댓글 개수가 0으로 보이는데 실제로는 댓글이 있음 | 열람 권한이 없는 요청에는 댓글 목록이 빈 배열(200)로 마스킹됨(KVE-2026-1914) | 정상 동작입니다. 작성자 본인 또는 `posts.read-secret`/관리 권한으로 조회하면 보입니다 |
|
||||
| 게시판 설정 일괄 적용 후 일부 게시판만 반영됨 | 대상 게시판 중 일부가 적용 도중 실패(예: 유효성 위반) | `settings.after_bulk_apply_aborted` 훅 시점의 로그로 실패한 게시판을 특정한 뒤 개별 재적용 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 변경 이력
|
||||
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,22 @@
|
||||
# 게시판 개발자 문서
|
||||
|
||||
> modules/_bundled/sirsoft-board · 모듈
|
||||
|
||||
<!-- @generated:stats START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**훅 수**: 90 · **구독 훅 수**: 77 · **라우트 수**: 80 · **모델 수**: 9 · **테이블 수**: 10 · **마이그레이션 수**: 30 · **레이아웃 수**: 46 · **핸들러 수**: 0
|
||||
<!-- @generated:stats END -->
|
||||
|
||||
## 문서 목차
|
||||
|
||||
<!-- @generated:doc-toc START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | 설계 의도·계층 지도·디렉토리 맵 |
|
||||
| [extension-points.md](extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 |
|
||||
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
|
||||
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
|
||||
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
|
||||
| [api/](api/README.md) | API 레퍼런스 |
|
||||
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
|
||||
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
|
||||
<!-- @generated:doc-toc END -->
|
||||
@@ -0,0 +1,75 @@
|
||||
# 게시판 — 아키텍처
|
||||
|
||||
> 설계 의도와 계층 구조 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설계 의도
|
||||
|
||||
<!-- @intent START -->
|
||||
게시판마다 완결된 권한·역할 체계를 갖게 하면서도, 새 게시판을 만드는 데 코드 변경이 필요 없게
|
||||
하는 것이 이 모듈의 핵심 설계 목표입니다. `boards` 테이블 한 행이 하나의 확장처럼 동작하도록,
|
||||
권한·역할·메뉴는 모두 **런타임에 게시판 데이터로부터 파생**됩니다(`getDynamicPermissionIdentifiers()`
|
||||
등 3개 메서드가 코드가 아니라 DB 를 읽어 계산). 그 대가로 이 세 메서드는 게시판이 하나
|
||||
추가/삭제될 때마다 정확해야 하고, 어긋나면 stale 권한이 남거나 존재하는 게시판의 권한이
|
||||
정리 대상으로 오판됩니다.
|
||||
|
||||
또한 "관리자 백엔드 모듈 + 방문자 화면은 템플릿" 분리를 의도적으로 유지합니다. 방문자 화면을
|
||||
이 모듈 안에 두면 템플릿마다 디자인이 다른 게시판 UI 를 이 모듈이 전부 알아야 하는데,
|
||||
API 로만 노출하면 템플릿 쪽에서 자유롭게 화면을 구성할 수 있습니다. 이 경계 때문에
|
||||
"레이아웃 확장"·"레이아웃"에는 오직 관리자 화면만 나타나며, 그것이 정상입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 계층 지도
|
||||
|
||||
<!-- @intent START -->
|
||||
```
|
||||
Http/Controllers (Admin/ 관리자, User/ 공개 API)
|
||||
│
|
||||
▼
|
||||
FormRequest (검증 + *_validation_rules 필터 훅으로 게시판별 규칙 확장 지점)
|
||||
│
|
||||
▼
|
||||
Services (BoardService/PostService/CommentService/ReportService/AttachmentService 등)
|
||||
│ before_* → filter_*_data → 실행 → after_* (전 도메인 공통 3단 훅 패턴)
|
||||
▼
|
||||
Repositories (RepositoryInterface 경유, 정렬 화이트리스트·컬럼 프루닝)
|
||||
│
|
||||
▼
|
||||
Models (SoftDeletes 적용 — Post/Comment/Attachment/Report)
|
||||
```
|
||||
|
||||
Listeners(`src/Listeners/`)는 이 흐름과 별도 레인입니다 — Service 가 발행한 훅을 받아 카운트
|
||||
동기화(`*CountSyncListener`)·활동 로그·SEO 캐시·검색 색인·알림 데이터 추출을 수행하며, Service
|
||||
자신은 이 부가효과를 알지 못합니다. 이 분리 덕분에 새 부가효과(예: 신규 카운트 컬럼 동기화)는
|
||||
Service 를 건드리지 않고 리스너 추가만으로 끝납니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 디렉토리
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `module.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `module.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Http/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Http/Resources/` | API 리소스 | 목록 응답은 화면이 실제로 그리는 것만 싣는다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Repositories/` | 데이터 접근 | 목록 쿼리는 컬럼 프루닝·정렬 화이트리스트 확인 |
|
||||
| `src/Models/` | Eloquent 모델 | 스키마 변경 시 마이그레이션 + 업그레이드 스텝 동반 |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/Enums/` | 상태·타입·분류 | 문자열 리터럴 대신 Enum 을 SSoT 로 둔다 |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `src/lang/` | 백엔드 다국어 | ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
| `database/migrations/` | 마이그레이션 | 한국어 comment + `down()` 필수, 기설치본은 업그레이드 스텝으로 백필 |
|
||||
| `database/seeders/` | 시더 | composer autoload 등록 + `extension:update-autoload` |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan module:update sirsoft-board --force` (빌드 불필요) |
|
||||
| `resources/routes/` | 라우트 → 레이아웃 매핑 (분할) | `php artisan module:update sirsoft-board --force` |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan module:build` → `php artisan module:update sirsoft-board --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan module:update sirsoft-board --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan module:update sirsoft-board --force` |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
<!-- @generated:directory-map END -->
|
||||
@@ -0,0 +1,161 @@
|
||||
# 게시판 — 데이터 모델
|
||||
|
||||
> 모델·소유 테이블·마이그레이션·Enum · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 모델
|
||||
|
||||
<!-- @generated:models START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 모델 | 테이블 | fillable | 관계 | 특성 |
|
||||
|---|---|---|---|---|
|
||||
| `Attachment` | `board_attachments` | 15 | board→Board, post→Post, creator→User | SoftDeletes |
|
||||
| `Board` | `boards` | 37 | creator→User, updater→User, posts→Post, comments→Comment, attachments→Attachment, reports→Report | - |
|
||||
| `BoardStat` | `board_stats` | 3 | - | - |
|
||||
| `BoardType` | `board_types` | 3 | - | HasUserOverrides |
|
||||
| `Comment` | `board_comments` | 14 | board→Board, post→Post, user→User, parent→self, replies→self | SoftDeletes |
|
||||
| `Post` | `board_posts` | 21 | board→Board, user→User, parent→self, replies→self, comments→Comment, attachments→Attachment, 외 1개 | SoftDeletes, 검색 색인 |
|
||||
| `Report` | `boards_reports` | 11 | board→Board, author→User, logs→ReportLog, processor→User | SoftDeletes |
|
||||
| `ReportLog` | `boards_report_logs` | 6 | report→Report, reporter→User | - |
|
||||
| `UserNotificationSetting` | `board_user_notification_settings` | 5 | user→User | - |
|
||||
<!-- @generated:models END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`Post`·`Comment` 모두 `parent`/`replies` 자기참조 관계를 갖습니다 — `Post` 의 자기참조는
|
||||
"답변형 게시판"(원글에 대한 관리자 답변)을, `Comment` 의 자기참조는 대댓글을 표현합니다. 둘은
|
||||
서로 다른 기능이라 관계 이름은 같아도 코드에서 섞어 쓰지 않습니다. `Board` 는 `HasUserOverrides`
|
||||
를 쓰지 **않습니다** — 게시판 자체는 운영자가 직접 소유·수정하는 리소스라 "모듈 재설치 시
|
||||
운영자 수정 보존"이 필요 없는 반면, `BoardType`(게시판 유형 프리셋)은 모듈이 시딩한 기본값을
|
||||
운영자가 손댈 수 있어야 하므로 그 트레이트를 씁니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 소유 테이블
|
||||
|
||||
<!-- @generated:tables START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 테이블 | 모델 |
|
||||
|---|---|
|
||||
| `board_attachments` | `Attachment` |
|
||||
| `board_comments` | `Comment` |
|
||||
| `board_mail_templates` | - |
|
||||
| `board_posts` | `Post` |
|
||||
| `board_stats` | `BoardStat` |
|
||||
| `board_types` | `BoardType` |
|
||||
| `board_user_notification_settings` | `UserNotificationSetting` |
|
||||
| `boards` | `Board` |
|
||||
| `boards_report_logs` | `ReportLog` |
|
||||
| `boards_reports` | `Report` |
|
||||
<!-- @generated:tables END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`board_mail_templates` 는 모델이 없는 채로 남아 있습니다 — 아래 마이그레이션 표의
|
||||
`drop_board_mail_templates_table`(2026-04-13)이 보여주듯, 메일 템플릿을 자체 테이블로
|
||||
관리하던 초기 설계를 코어 `GenericNotification` 알림 정의(§알림 정의)로 이관하며 테이블만
|
||||
드롭하고 이름은 이력상 남아 있는 상태입니다. 신규 코드에서 이 이름을 참조하지 않습니다.
|
||||
테이블 접두어가 `board_*`와 `boards_*` 두 가지로 섞여 있는 것은 설계 의도가 아니라 이력입니다
|
||||
— 새 테이블을 추가할 때는 `board_*`(단수)를 따릅니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 마이그레이션
|
||||
|
||||
<!-- @generated:migrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
마이그레이션 30개.
|
||||
|
||||
| 파일 | 생성 테이블 | 변경 테이블 | down() |
|
||||
|---|---|---|---|
|
||||
| `2026_04_01_000001_create_board_types_table.php` | `board_types` | - | ✅ |
|
||||
| `2026_04_01_000002_create_boards_table.php` | `boards` | `boards` | ✅ |
|
||||
| `2026_04_01_000003_create_board_user_notification_settings_table.php` | `board_user_notification_settings` | `board_user_notification_settings` | ✅ |
|
||||
| `2026_04_01_000004_create_board_posts_table.php` | `board_posts` | - | ✅ |
|
||||
| `2026_04_01_000005_create_board_comments_table.php` | `board_comments` | - | ✅ |
|
||||
| `2026_04_01_000006_create_board_attachments_table.php` | `board_attachments` | - | ✅ |
|
||||
| `2026_04_01_000007_create_boards_reports_table.php` | `boards_reports` | `boards_reports` | ✅ |
|
||||
| `2026_04_01_000008_create_boards_report_logs_table.php` | `boards_report_logs` | `boards_report_logs` | ✅ |
|
||||
| `2026_04_01_000009_create_board_mail_templates_table.php` | `board_mail_templates` | - | ✅ |
|
||||
| `2026_04_01_000010_add_fulltext_indexes_to_boards_table.php` | - | `boards` | ✅ |
|
||||
| `2026_04_01_000011_add_fulltext_indexes_to_boards_report_logs_table.php` | - | `boards_report_logs` | ✅ |
|
||||
| `2026_04_01_000012_add_indexes_to_board_posts_table.php` | - | `board_posts` | ✅ |
|
||||
| `2026_04_13_000001_drop_board_mail_templates_table.php` | `board_mail_templates` | - | ✅ |
|
||||
| `2026_04_13_000002_add_user_overrides_to_board_types_table.php` | - | `board_types` | ✅ |
|
||||
| `2026_04_14_000001_drop_channel_columns_from_boards_table.php` | - | `boards` | ✅ |
|
||||
| `2026_04_17_000001_remove_partitions_from_board_tables.php` | - | - | ✅ |
|
||||
| `2026_04_17_000002_add_count_columns_to_board_tables.php` | - | `board_posts`, `board_comments` | ✅ |
|
||||
| `2026_04_17_000003_add_posts_count_and_comments_count_to_boards_table.php` | - | `boards` | ✅ |
|
||||
| `2026_04_17_000004_update_indexes_in_board_tables.php` | - | `board_posts`, `board_comments`, `board_attachments` | ✅ |
|
||||
| `2026_05_29_000001_create_board_stats_table.php` | `board_stats` | - | ✅ |
|
||||
| `2026_06_08_000001_add_recent_across_boards_index_to_board_posts.php` | - | `board_posts` | ✅ |
|
||||
| `2026_06_26_000001_modify_trigger_type_in_board_comments_table.php` | - | - | ✅ |
|
||||
| `2026_06_26_000002_add_trigger_type_to_board_attachments_table.php` | - | `board_attachments` | ✅ |
|
||||
| `2026_08_01_000001_add_tiebreak_to_board_posts_list_index.php` | - | - | ✅ |
|
||||
| `2026_08_01_000002_add_list_sort_index_to_boards_reports_table.php` | - | - | ✅ |
|
||||
| `2026_08_06_000001_update_max_reply_depth_comment_in_boards_table.php` | - | - | ✅ |
|
||||
| `2026_08_06_000002_add_view_count_sort_index_to_board_posts_table.php` | - | - | ✅ |
|
||||
| `2026_08_17_000001_add_reply_delete_policy_to_boards_table.php` | - | `boards` | ✅ |
|
||||
| `2026_08_17_000002_modify_trigger_type_in_board_posts_table.php` | - | - | ✅ |
|
||||
| `2026_08_22_000001_add_content_thumbnail_url_to_board_posts_table.php` | - | `board_posts` | ✅ |
|
||||
<!-- @generated:migrations END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
목록 성능 마이그레이션이 지속적으로 추가되는 것(인덱스 4건 + 정렬 타이브레이크 2건)은 우연이
|
||||
아닙니다 — 게시글 목록은 공지 제외·답변 제외 필터가 항상 걸린 채로 `created_at`/`view_count`
|
||||
2가지 정렬을 지원해야 하므로, 새 정렬 옵션을 추가할 때마다 그 정렬에 맞는 복합 인덱스를 함께
|
||||
마이그레이션합니다(`getBenchmarkProfiles()` 의 `board_posts_by_view_count` 프로파일이 바로 이
|
||||
목적으로 존재합니다). `remove_partitions_from_board_tables`(2026-04-17)는 `board_posts`/`board_comments`/
|
||||
`board_attachments` 3개 테이블에 적용했던 파티셔닝을 되돌린 이력입니다 — 그 마이그레이션의
|
||||
`down()` 은 "파티션 복원은 데이터 재배치가 필요해 자동 롤백 불가"라고 명시하므로, 파티셔닝을
|
||||
다시 도입할 때는 이 파일을 그대로 재실행하는 방식이 아니라 새 마이그레이션으로 설계해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Enum
|
||||
|
||||
<!-- @generated:enums START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| Enum | backing | case 수 | case |
|
||||
|---|---|---|---|
|
||||
| `BoardOrderBy` | `string` | 4 | `created_at`, `view_count`, `title`, `author` |
|
||||
| `OrderDirection` | `string` | 2 | `ASC`, `DESC` |
|
||||
| `PostStatus` | `string` | 3 | `published`, `blinded`, `deleted` |
|
||||
| `ReplyDeletePolicy` | `string` | 2 | `block`, `cascade` |
|
||||
| `ReportReasonType` | `string` | 9 | `abuse`, `hate_speech`, `spam`, `copyright`, `privacy`, `misinformation`, `sexual`, `violence`, `외 1개` |
|
||||
| `ReportStatus` | `string` | 5 | `pending`, `review`, `rejected`, `suspended`, `deleted` |
|
||||
| `ReportType` | `string` | 2 | `post`, `comment` |
|
||||
| `SecretMode` | `string` | 3 | `disabled`, `enabled`, `always` |
|
||||
| `TriggerType` | `string` | 6 | `report`, `admin`, `system`, `auto_hide`, `user`, `cascade` |
|
||||
<!-- @generated:enums END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`TriggerType`(6 case: report/admin/system/auto_hide/user/cascade)은 "이 콘텐츠가 왜 지금
|
||||
상태가 됐는가"를 기록하는 감사(audit) 축입니다 — 예를 들어 게시글 블라인드가 `report`(신고
|
||||
처리 결과)인지 `admin`(관리자 직접 조치)인지에 따라 `post_action`/`report_action` 두 알림이
|
||||
갈라집니다(§확장점 "알림 정의" 참고). `cascade` 는 부모(게시글)가 지워질 때 자식(댓글)이
|
||||
함께 지워진 경우이며, `ReplyDeletePolicy`(`block`/`cascade`)가 게시판별로 부모 삭제 시
|
||||
자식을 막을지 함께 지울지를 결정합니다 — 이 정책과 `TriggerType::Cascade` 는 같은 흐름의
|
||||
서로 다른 절반(정책 설정 vs 결과 기록)입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Repository
|
||||
|
||||
<!-- @generated:repositories START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 클래스 | 종류 | 설명 |
|
||||
|---|---|---|
|
||||
| `AttachmentRepository` | 구현 | 게시판 첨부파일 Repository 구현체 |
|
||||
| `AttachmentRepositoryInterface` | 인터페이스 | 게시판 첨부파일 Repository 인터페이스 |
|
||||
| `BoardRepository` | 구현 | 게시판 Repository |
|
||||
| `BoardRepositoryInterface` | 인터페이스 | 게시판 Repository 인터페이스 |
|
||||
| `BoardStatRepository` | 구현 | 게시판 일별 집계 Repository |
|
||||
| `BoardStatRepositoryInterface` | 인터페이스 | 게시판 일별 집계 Repository 인터페이스 |
|
||||
| `BoardTypeRepository` | 구현 | - |
|
||||
| `BoardTypeRepositoryInterface` | 인터페이스 | - |
|
||||
| `CommentRepository` | 구현 | 댓글 Repository |
|
||||
| `CommentRepositoryInterface` | 인터페이스 | 댓글 Repository 인터페이스 |
|
||||
| `PostRepository` | 구현 | 게시글 Repository |
|
||||
| `PostRepositoryInterface` | 인터페이스 | 게시글 Repository 인터페이스 |
|
||||
| `ReportRepository` | 구현 | 신고 Repository |
|
||||
| `ReportRepositoryInterface` | 인터페이스 | 신고 Repository 인터페이스 |
|
||||
| `UserNotificationSettingRepository` | 구현 | 사용자 알림 설정 Repository |
|
||||
| `UserNotificationSettingRepositoryInterface` | 인터페이스 | 사용자 알림 설정 Repository 인터페이스 |
|
||||
<!-- @generated:repositories END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`BoardStatRepository`(일별 집계)는 별도 Repository 로 분리돼 있습니다 — `sirsoft-board:aggregate-stats`
|
||||
스케줄이 매시간 `board_stats` 를 갱신하는데, 이 집계 쿼리를 `PostRepository`/`CommentRepository`
|
||||
에 섞으면 대시보드 조회 경로와 실시간 CRUD 경로가 같은 클래스 안에서 뒤엉킵니다. 새 Repository
|
||||
를 추가할 때는 반드시 인터페이스를 함께 만들고 `CoreServiceProvider`(또는 이 모듈의
|
||||
서비스 프로바이더)에서 바인딩합니다 — Service 가 구체 클래스를 직접 타입힌트하면 안 됩니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,323 @@
|
||||
# 게시판 — 확장점
|
||||
|
||||
> 발행/구독 훅·미들웨어·채널·스케줄 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 발행 훅
|
||||
|
||||
<!-- @generated:hooks-published START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
발행 훅 90종 / 호출 지점 91곳. 이 중 90종은 `getHooks()` 선언에 없어 소스에서 자동 감지한 것입니다 — 선언에 추가하면 유형과 설명이 함께 실립니다.
|
||||
|
||||
| 훅 이름 | 유형 | 설명 | 발행 위치 |
|
||||
|---|---|---|---|
|
||||
| `core.module_settings.after_save` | action | — | `src/Http/Controllers/Admin/BoardSettingsController.php:123` |
|
||||
| `sirsoft-board.attachment.after_delete` | action | — | `src/Services/AttachmentService.php:463` |
|
||||
| `sirsoft-board.attachment.after_download` | action | — | `src/Services/AttachmentService.php:776` |
|
||||
| `sirsoft-board.attachment.after_link` | action | — | `src/Services/AttachmentService.php:217` 외 1곳 |
|
||||
| `sirsoft-board.attachment.after_reorder` | action | — | `src/Services/AttachmentService.php:631` |
|
||||
| `sirsoft-board.attachment.after_upload` | action | — | `src/Services/AttachmentService.php:192` |
|
||||
| `sirsoft-board.attachment.before_delete` | action | — | `src/Services/AttachmentService.php:442` |
|
||||
| `sirsoft-board.attachment.before_reorder` | action | — | `src/Services/AttachmentService.php:626` |
|
||||
| `sirsoft-board.attachment.before_upload` | action | — | `src/Services/AttachmentService.php:117` |
|
||||
| `sirsoft-board.attachment.filter_upload_file` | filter | — | `src/Services/AttachmentService.php:120` |
|
||||
| `sirsoft-board.attachment.reorder_validation_rules` | filter | — | `src/Http/Requests/ReorderAttachmentsRequest.php:35` |
|
||||
| `sirsoft-board.attachment.upload_validation_rules` | filter | — | `src/Http/Requests/UploadAttachmentRequest.php:77` |
|
||||
| `sirsoft-board.board.after_add_to_menu` | action | — | `src/Services/BoardService.php:1059` |
|
||||
| `sirsoft-board.board.after_create` | action | — | `src/Services/BoardService.php:457` |
|
||||
| `sirsoft-board.board.after_delete` | action | — | `src/Services/BoardService.php:623` |
|
||||
| `sirsoft-board.board.after_remove_from_menu` | action | — | `src/Services/BoardService.php:1104` |
|
||||
| `sirsoft-board.board.after_update` | action | — | `src/Services/BoardService.php:543` |
|
||||
| `sirsoft-board.board.before_add_to_menu` | action | — | `src/Services/BoardService.php:1023` |
|
||||
| `sirsoft-board.board.before_copy` | action | — | `src/Services/BoardService.php:646` |
|
||||
| `sirsoft-board.board.before_create` | action | — | `src/Services/BoardService.php:382` |
|
||||
| `sirsoft-board.board.before_delete` | action | — | `src/Services/BoardService.php:574` |
|
||||
| `sirsoft-board.board.before_remove_from_menu` | action | — | `src/Services/BoardService.php:1090` |
|
||||
| `sirsoft-board.board.before_update` | action | — | `src/Services/BoardService.php:495` |
|
||||
| `sirsoft-board.board.filter_copy_data` | filter | — | `src/Services/BoardService.php:703` |
|
||||
| `sirsoft-board.board.filter_create_data` | filter | — | `src/Services/BoardService.php:385` |
|
||||
| `sirsoft-board.board.filter_menu_data` | filter | — | `src/Services/BoardService.php:1053` |
|
||||
| `sirsoft-board.board.filter_update_data` | filter | — | `src/Services/BoardService.php:500` |
|
||||
| `sirsoft-board.board.posts.before_force_delete` | action | — | `src/Services/BoardService.php:593` |
|
||||
| `sirsoft-board.board.store_validation_rules` | filter | — | `src/Http/Requests/StoreBoardRequest.php:225` |
|
||||
| `sirsoft-board.board.update_validation_rules` | filter | — | `src/Http/Requests/UpdateBoardRequest.php:228` |
|
||||
| `sirsoft-board.board_type.after_create` | action | — | `src/Services/BoardTypeService.php:42` |
|
||||
| `sirsoft-board.board_type.after_delete` | action | — | `src/Services/BoardTypeService.php:107` |
|
||||
| `sirsoft-board.board_type.after_update` | action | — | `src/Services/BoardTypeService.php:70` |
|
||||
| `sirsoft-board.board_type.before_create` | action | — | `src/Services/BoardTypeService.php:36` |
|
||||
| `sirsoft-board.board_type.before_delete` | action | — | `src/Services/BoardTypeService.php:103` |
|
||||
| `sirsoft-board.board_type.before_update` | action | — | `src/Services/BoardTypeService.php:62` |
|
||||
| `sirsoft-board.board_type.filter_create_data` | filter | — | `src/Services/BoardTypeService.php:38` |
|
||||
| `sirsoft-board.board_type.filter_update_data` | filter | — | `src/Services/BoardTypeService.php:66` |
|
||||
| `sirsoft-board.comment.after_blind` | action | — | `src/Services/CommentService.php:480` |
|
||||
| `sirsoft-board.comment.after_create` | action | — | `src/Services/CommentService.php:375` |
|
||||
| `sirsoft-board.comment.after_delete` | action | — | `src/Services/CommentService.php:441` |
|
||||
| `sirsoft-board.comment.after_restore` | action | — | `src/Services/CommentService.php:516` |
|
||||
| `sirsoft-board.comment.after_update` | action | — | `src/Services/CommentService.php:410` |
|
||||
| `sirsoft-board.comment.before_blind` | action | — | `src/Services/CommentService.php:471` |
|
||||
| `sirsoft-board.comment.before_create` | action | — | `src/Services/CommentService.php:341` |
|
||||
| `sirsoft-board.comment.before_delete` | action | — | `src/Services/CommentService.php:431` |
|
||||
| `sirsoft-board.comment.before_restore` | action | — | `src/Services/CommentService.php:507` |
|
||||
| `sirsoft-board.comment.before_update` | action | — | `src/Services/CommentService.php:399` |
|
||||
| `sirsoft-board.comment.filter_create_data` | filter | — | `src/Services/CommentService.php:344` |
|
||||
| `sirsoft-board.comment.filter_update_data` | filter | — | `src/Services/CommentService.php:404` |
|
||||
| `sirsoft-board.comment.store_validation_rules` | filter | — | `src/Http/Requests/StoreCommentRequest.php:91` |
|
||||
| `sirsoft-board.comment.update_validation_rules` | filter | — | `src/Http/Requests/UpdateCommentRequest.php:67` |
|
||||
| `sirsoft-board.permissions.after_create` | action | — | `src/Services/BoardService.php:431` |
|
||||
| `sirsoft-board.permissions.after_delete` | action | — | `src/Services/BoardService.php:600` |
|
||||
| `sirsoft-board.permissions.after_update` | action | — | `src/Services/BoardService.php:523` |
|
||||
| `sirsoft-board.post.after_blind` | action | — | `src/Services/PostService.php:500` |
|
||||
| `sirsoft-board.post.after_create` | action | — | `src/Services/PostService.php:275` |
|
||||
| `sirsoft-board.post.after_delete` | action | — | `src/Services/PostService.php:459` |
|
||||
| `sirsoft-board.post.after_restore` | action | — | `src/Services/PostService.php:564` |
|
||||
| `sirsoft-board.post.after_update` | action | — | `src/Services/PostService.php:351` |
|
||||
| `sirsoft-board.post.before_blind` | action | — | `src/Services/PostService.php:491` |
|
||||
| `sirsoft-board.post.before_create` | action | — | `src/Services/PostService.php:250` |
|
||||
| `sirsoft-board.post.before_delete` | action | — | `src/Services/PostService.php:426` |
|
||||
| `sirsoft-board.post.before_restore` | action | — | `src/Services/PostService.php:533` |
|
||||
| `sirsoft-board.post.before_update` | action | — | `src/Services/PostService.php:324` |
|
||||
| `sirsoft-board.post.filter_content_thumbnail` | filter | — | `src/Models/Post.php:138` |
|
||||
| `sirsoft-board.post.filter_create_data` | filter | — | `src/Services/PostService.php:253` |
|
||||
| `sirsoft-board.post.filter_update_data` | filter | — | `src/Services/PostService.php:329` |
|
||||
| `sirsoft-board.post.store_validation_rules` | filter | — | `src/Http/Requests/StorePostRequest.php:120` |
|
||||
| `sirsoft-board.post.update_validation_rules` | filter | — | `src/Http/Requests/UpdatePostRequest.php:77` |
|
||||
| `sirsoft-board.report.after_blind_content` | action | — | `src/Services/ReportService.php:712` |
|
||||
| `sirsoft-board.report.after_bulk_update_status` | action | — | `src/Services/ReportService.php:492` |
|
||||
| `sirsoft-board.report.after_create` | action | — | `src/Services/ReportService.php:292` |
|
||||
| `sirsoft-board.report.after_delete` | action | — | `src/Services/ReportService.php:559` |
|
||||
| `sirsoft-board.report.after_delete_content` | action | — | `src/Services/ReportService.php:880` |
|
||||
| `sirsoft-board.report.after_restore_content` | action | — | `src/Services/ReportService.php:678` |
|
||||
| `sirsoft-board.report.after_update_status` | action | — | `src/Services/ReportService.php:369` |
|
||||
| `sirsoft-board.report.before_bulk_update_status` | action | — | `src/Services/ReportService.php:385` |
|
||||
| `sirsoft-board.report.before_create` | action | — | `src/Services/ReportService.php:203` |
|
||||
| `sirsoft-board.report.before_delete` | action | — | `src/Services/ReportService.php:554` |
|
||||
| `sirsoft-board.report.before_update_status` | action | — | `src/Services/ReportService.php:324` |
|
||||
| `sirsoft-board.report.filter_create_data` | filter | — | `src/Services/ReportService.php:227` |
|
||||
| `sirsoft-board.roles.after_create` | action | — | `src/Services/BoardService.php:411` |
|
||||
| `sirsoft-board.roles.after_delete` | action | — | `src/Services/BoardService.php:604` |
|
||||
| `sirsoft-board.search.post.index_should_update` | filter | — | `src/Models/Post.php:280` |
|
||||
| `sirsoft-board.settings.after_bulk_apply` | action | — | `src/Services/BoardService.php:1368` |
|
||||
| `sirsoft-board.settings.after_bulk_apply_aborted` | action | — | `src/Services/BoardService.php:1359` |
|
||||
| `sirsoft-board.settings.before_bulk_apply` | action | — | `src/Services/BoardService.php:1263` |
|
||||
| `sirsoft-board.user_post.store_validation_rules` | filter | — | `src/Http/Requests/User/StorePostRequest.php:162` |
|
||||
| `sirsoft-board.user_post.update_validation_rules` | filter | — | `src/Http/Requests/User/UpdatePostRequest.php:112` |
|
||||
<!-- @generated:hooks-published END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`{도메인}.{동사}` 이름 규칙 안에서 4개 도메인(board/post/comment/report)이 전부 같은 3단
|
||||
패턴(`before_*` → `filter_*_data` → `after_*`)을 반복합니다. 새 검증 규칙을 게시판별로 다르게
|
||||
걸고 싶으면 `*.store_validation_rules`/`update_validation_rules` filter 를, 저장 직전 데이터를
|
||||
가공하고 싶으면 `filter_*_data` 를, 저장 완료 후 부가 작업(알림·외부 연동)을 붙이고 싶으면
|
||||
`after_*` action 을 잡습니다 — `before_*` 에서 예외를 던지면 저장 자체를 막을 수 있습니다.
|
||||
|
||||
`sirsoft-board.post.filter_content_thumbnail`/`sirsoft-board.search.post.index_should_update`
|
||||
두 필터는 Model(`Post.php`) 안에서 직접 발행되는 예외적인 자리입니다 — Service 를 거치지 않는
|
||||
지연 평가(썸네일 추출, 검색 색인 갱신 여부 판단)라 Model 이 직접 훅을 겁니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 구독 훅
|
||||
|
||||
<!-- @generated:hooks-subscribed START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 훅 이름 | 유형 | 리스너 | 메서드 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| `core.activity_log.filter_description_params` | filter | `ActivityLogDescriptionResolver` | `resolveDescriptionParams` | 10 |
|
||||
| `core.module_settings.after_save` | action (미선언) | `SeoBoardSettingsCacheListener` | `onModuleSettingsSave` | 20 |
|
||||
| `core.notification.filter_default_definitions` | filter | `BoardNotificationDataListener` | `contributeDefaultDefinitions` | 20 |
|
||||
| `core.search.build_response` | filter | `SearchPostsListener` | `buildPostsResponse` | 10 |
|
||||
| `core.search.index_validation_rules` | filter | `SearchPostsListener` | `addValidationRules` | 10 |
|
||||
| `core.search.results` | filter | `SearchPostsListener` | `searchPosts` | 10 |
|
||||
| `core.user.after_create` | action (미선언) | `UserNotificationSettingsListener` | `afterCreate` | 10 |
|
||||
| `core.user.create_validation_rules` | filter | `UserNotificationSettingsListener` | `addValidationRules` | 10 |
|
||||
| `core.user.filter_create_data` | filter | `UserNotificationSettingsListener` | `filterCreateData` | 10 |
|
||||
| `core.user.filter_resource_data` | filter | `UserNotificationSettingsListener` | `filterResourceData` | 10 |
|
||||
| `core.user.filter_update_data` | filter | `UserNotificationSettingsListener` | `filterUpdateData` | 10 |
|
||||
| `core.user.update_profile_validation_rules` | filter | `UserNotificationSettingsListener` | `addValidationRules` | 10 |
|
||||
| `core.user.update_validation_rules` | filter | `UserNotificationSettingsListener` | `addValidationRules` | 10 |
|
||||
| `sirsoft-board.attachment.after_delete` | action (미선언) | `BoardActivityLogListener` | `handleAttachmentAfterDelete` | 20 |
|
||||
| `sirsoft-board.attachment.after_delete` | action (미선언) | `PostAttachmentCountSyncListener` | `syncAttachmentsCount` | 10 |
|
||||
| `sirsoft-board.attachment.after_download` | action (미선언) | `BoardActivityLogListener` | `handleAttachmentAfterDownload` | 20 |
|
||||
| `sirsoft-board.attachment.after_link` | action (미선언) | `PostAttachmentCountSyncListener` | `syncAttachmentsCount` | 10 |
|
||||
| `sirsoft-board.attachment.after_upload` | action (미선언) | `BoardActivityLogListener` | `handleAttachmentAfterUpload` | 20 |
|
||||
| `sirsoft-board.attachment.after_upload` | action (미선언) | `PostAttachmentCountSyncListener` | `syncAttachmentsCount` | 10 |
|
||||
| `sirsoft-board.board.after_add_to_menu` | action (미선언) | `BoardActivityLogListener` | `handleBoardAfterAddToMenu` | 20 |
|
||||
| `sirsoft-board.board.after_create` | action (미선언) | `BoardActivityLogListener` | `handleBoardAfterCreate` | 20 |
|
||||
| `sirsoft-board.board.after_delete` | action (미선언) | `BoardActivityLogListener` | `handleBoardAfterDelete` | 20 |
|
||||
| `sirsoft-board.board.after_remove_from_menu` | action (미선언) | `BoardActivityLogListener` | `handleBoardAfterRemoveFromMenu` | 20 |
|
||||
| `sirsoft-board.board.after_update` | action (미선언) | `BoardActivityLogListener` | `handleBoardAfterUpdate` | 20 |
|
||||
| `sirsoft-board.board.after_update` | action (미선언) | `SeoBoardCacheListener` | `onBoardUpdate` | 20 |
|
||||
| `sirsoft-board.board_type.after_create` | action (미선언) | `BoardActivityLogListener` | `handleBoardTypeAfterCreate` | 20 |
|
||||
| `sirsoft-board.board_type.after_delete` | action (미선언) | `BoardActivityLogListener` | `handleBoardTypeAfterDelete` | 20 |
|
||||
| `sirsoft-board.board_type.after_update` | action (미선언) | `BoardActivityLogListener` | `handleBoardTypeAfterUpdate` | 20 |
|
||||
| `sirsoft-board.comment.after_blind` | action (미선언) | `BoardActivityLogListener` | `handleCommentAfterBlind` | 20 |
|
||||
| `sirsoft-board.comment.after_create` | action (미선언) | `BoardActivityLogListener` | `handleCommentAfterCreate` | 20 |
|
||||
| `sirsoft-board.comment.after_create` | action (미선언) | `BoardCommentsCountSyncListener` | `syncCommentsCount` | 10 |
|
||||
| `sirsoft-board.comment.after_create` | action (미선언) | `CommentReplySyncListener` | `syncRepliesCount` | 10 |
|
||||
| `sirsoft-board.comment.after_create` | action (미선언) | `PostCountSyncListener` | `syncCommentsCount` | 10 |
|
||||
| `sirsoft-board.comment.after_delete` | action (미선언) | `BoardActivityLogListener` | `handleCommentAfterDelete` | 20 |
|
||||
| `sirsoft-board.comment.after_delete` | action (미선언) | `BoardCommentsCountSyncListener` | `syncCommentsCount` | 10 |
|
||||
| `sirsoft-board.comment.after_delete` | action (미선언) | `CommentReplySyncListener` | `syncRepliesCount` | 10 |
|
||||
| `sirsoft-board.comment.after_delete` | action (미선언) | `PostCountSyncListener` | `syncCommentsCount` | 10 |
|
||||
| `sirsoft-board.comment.after_restore` | action (미선언) | `BoardActivityLogListener` | `handleCommentAfterRestore` | 20 |
|
||||
| `sirsoft-board.comment.after_restore` | action (미선언) | `BoardCommentsCountSyncListener` | `syncCommentsCount` | 10 |
|
||||
| `sirsoft-board.comment.after_restore` | action (미선언) | `CommentReplySyncListener` | `syncRepliesCount` | 10 |
|
||||
| `sirsoft-board.comment.after_restore` | action (미선언) | `PostCountSyncListener` | `syncCommentsCount` | 10 |
|
||||
| `sirsoft-board.comment.after_update` | action (미선언) | `BoardActivityLogListener` | `handleCommentAfterUpdate` | 20 |
|
||||
| `sirsoft-board.notification.channels` | filter | `BoardNotificationChannelListener` | `filterChannels` | 10 |
|
||||
| `sirsoft-board.notification.extract_data` | filter | `BoardNotificationDataListener` | `extractData` | 20 |
|
||||
| `sirsoft-board.post.after_blind` | action (미선언) | `BoardActivityLogListener` | `handlePostAfterBlind` | 20 |
|
||||
| `sirsoft-board.post.after_create` | action (미선언) | `BoardActivityLogListener` | `handlePostAfterCreate` | 20 |
|
||||
| `sirsoft-board.post.after_create` | action (미선언) | `BoardPostsCountSyncListener` | `syncPostsCount` | 10 |
|
||||
| `sirsoft-board.post.after_create` | action (미선언) | `PostReplySyncListener` | `syncRepliesCount` | 10 |
|
||||
| `sirsoft-board.post.after_create` | action (미선언) | `SeoBoardCacheListener` | `onPostCreate` | 20 |
|
||||
| `sirsoft-board.post.after_delete` | action (미선언) | `BoardActivityLogListener` | `handlePostAfterDelete` | 20 |
|
||||
| `sirsoft-board.post.after_delete` | action (미선언) | `BoardPostsCountSyncListener` | `syncPostsCount` | 10 |
|
||||
| `sirsoft-board.post.after_delete` | action (미선언) | `PostReplySyncListener` | `syncRepliesCount` | 10 |
|
||||
| `sirsoft-board.post.after_delete` | action (미선언) | `SeoBoardCacheListener` | `onPostDelete` | 20 |
|
||||
| `sirsoft-board.post.after_restore` | action (미선언) | `BoardActivityLogListener` | `handlePostAfterRestore` | 20 |
|
||||
| `sirsoft-board.post.after_restore` | action (미선언) | `BoardPostsCountSyncListener` | `syncPostsCount` | 10 |
|
||||
| `sirsoft-board.post.after_restore` | action (미선언) | `PostReplySyncListener` | `syncRepliesCount` | 10 |
|
||||
| `sirsoft-board.post.after_update` | action (미선언) | `BoardActivityLogListener` | `handlePostAfterUpdate` | 20 |
|
||||
| `sirsoft-board.post.after_update` | action (미선언) | `SeoBoardCacheListener` | `onPostUpdate` | 20 |
|
||||
| `sirsoft-board.report.after_blind_content` | action (미선언) | `BoardActivityLogListener` | `handleReportAfterBlindContent` | 20 |
|
||||
| `sirsoft-board.report.after_bulk_update_status` | action (미선언) | `BoardActivityLogListener` | `handleReportAfterBulkUpdateStatus` | 20 |
|
||||
| `sirsoft-board.report.after_create` | action (미선언) | `BoardActivityLogListener` | `handleReportAfterCreate` | 20 |
|
||||
| `sirsoft-board.report.after_delete` | action (미선언) | `BoardActivityLogListener` | `handleReportAfterDelete` | 20 |
|
||||
| `sirsoft-board.report.after_delete_content` | action (미선언) | `BoardActivityLogListener` | `handleReportAfterDeleteContent` | 20 |
|
||||
| `sirsoft-board.report.after_restore_content` | action (미선언) | `BoardActivityLogListener` | `handleReportAfterRestoreContent` | 20 |
|
||||
| `sirsoft-board.report.after_update_status` | action (미선언) | `BoardActivityLogListener` | `handleReportAfterUpdateStatus` | 20 |
|
||||
| `sirsoft-board.settings.after_bulk_apply` | action (미선언) | `BoardActivityLogListener` | `handleSettingsAfterBulkApply` | 20 |
|
||||
| `sirsoft-board.settings.after_bulk_apply` | action (미선언) | `SeoBoardSettingsCacheListener` | `onBulkApply` | 20 |
|
||||
| `sirsoft-board.settings.after_bulk_apply_aborted` | action (미선언) | `BoardActivityLogListener` | `handleSettingsAfterBulkApplyAborted` | 20 |
|
||||
| `sirsoft-ckeditor5.image.filter_reference_sources` | filter | `Ckeditor5ReferenceSourcesListener` | `addBoardSources` | 10 |
|
||||
| `sirsoft-ecommerce.inquiry.count_replies` | filter | `EcommerceInquiryHookListener` | `countReplies` | 10 |
|
||||
| `sirsoft-ecommerce.inquiry.create` | filter | `EcommerceInquiryHookListener` | `createAndReturn` | 10 |
|
||||
| `sirsoft-ecommerce.inquiry.delete` | filter | `EcommerceInquiryHookListener` | `deletePost` | 10 |
|
||||
| `sirsoft-ecommerce.inquiry.delete_reply` | filter | `EcommerceInquiryHookListener` | `deleteReplyPost` | 10 |
|
||||
| `sirsoft-ecommerce.inquiry.get_by_ids` | filter | `EcommerceInquiryHookListener` | `getByIds` | 10 |
|
||||
| `sirsoft-ecommerce.inquiry.get_settings` | filter | `EcommerceInquiryHookListener` | `getBoardSettings` | 10 |
|
||||
| `sirsoft-ecommerce.inquiry.update` | filter | `EcommerceInquiryHookListener` | `updatePost` | 10 |
|
||||
| `sirsoft-ecommerce.inquiry.update_reply` | filter | `EcommerceInquiryHookListener` | `updateReplyPost` | 10 |
|
||||
<!-- @generated:hooks-subscribed END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`core.user.*` 4종을 구독하는 이유는 회원가입/수정 화면에 "댓글 알림 수신 여부" 필드를 끼워
|
||||
넣기 위해서입니다 — 이 필드는 `UserNotificationSetting` 모델(board 소유)에 저장되지만, 입력
|
||||
자체는 코어 회원 폼에서 받습니다. `sirsoft-ckeditor5.image.filter_reference_sources` 구독은
|
||||
board 글 본문(HTML 에디터)에 삽입된 이미지가 삭제 시 함께 정리되도록 참조 소스 목록에 게시글을
|
||||
등록하는 자리입니다. `sirsoft-ecommerce.inquiry.*` 8개는 이커머스 "상품 문의"가 board 의
|
||||
Post/Comment CRUD 를 그대로 재사용하되 저장 로직만 이커머스가 대신 처리하는 위임 지점입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 훅 리스너
|
||||
|
||||
<!-- @generated:listeners START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 리스너 | 구독 훅 | 등록 방식 | HookListenerInterface | 파일 |
|
||||
|---|---|---|---|---|
|
||||
| `ActivityLogDescriptionResolver` | 1개 | 명시 등록 | ✅ | `src/Listeners/ActivityLogDescriptionResolver.php` |
|
||||
| `BoardActivityLogListener` | 30개 | 명시 등록 | ✅ | `src/Listeners/BoardActivityLogListener.php` |
|
||||
| `BoardCommentsCountSyncListener` | 3개 | 명시 등록 | ✅ | `src/Listeners/BoardCommentsCountSyncListener.php` |
|
||||
| `BoardNotificationChannelListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/BoardNotificationChannelListener.php` |
|
||||
| `BoardNotificationDataListener` | 2개 | 명시 등록 | ✅ | `src/Listeners/BoardNotificationDataListener.php` |
|
||||
| `BoardPostsCountSyncListener` | 3개 | 명시 등록 | ✅ | `src/Listeners/BoardPostsCountSyncListener.php` |
|
||||
| `Ckeditor5ReferenceSourcesListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/Ckeditor5ReferenceSourcesListener.php` |
|
||||
| `CommentReplySyncListener` | 3개 | 명시 등록 | ✅ | `src/Listeners/CommentReplySyncListener.php` |
|
||||
| `EcommerceInquiryHookListener` | 8개 | 명시 등록 | ✅ | `src/Listeners/EcommerceInquiryHookListener.php` |
|
||||
| `PostAttachmentCountSyncListener` | 3개 | 명시 등록 | ✅ | `src/Listeners/PostAttachmentCountSyncListener.php` |
|
||||
| `PostCountSyncListener` | 3개 | 명시 등록 | ✅ | `src/Listeners/PostCountSyncListener.php` |
|
||||
| `PostReplySyncListener` | 3개 | 명시 등록 | ✅ | `src/Listeners/PostReplySyncListener.php` |
|
||||
| `SearchPostsListener` | 3개 | 명시 등록 | ✅ | `src/Listeners/SearchPostsListener.php` |
|
||||
| `SeoBoardCacheListener` | 4개 | 명시 등록 | ✅ | `src/Listeners/SeoBoardCacheListener.php` |
|
||||
| `SeoBoardSettingsCacheListener` | 2개 | 명시 등록 | ✅ | `src/Listeners/SeoBoardSettingsCacheListener.php` |
|
||||
| `UserNotificationSettingsListener` | 7개 | 명시 등록 | ✅ | `src/Listeners/UserNotificationSettingsListener.php` |
|
||||
<!-- @generated:listeners END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`BoardActivityLogListener` 하나가 30개 훅을 구독하는 것이 의도된 형태입니다 — 활동 로그는
|
||||
"무엇이 언제 왜 바뀌었는가"를 도메인 전체에서 일관된 형식으로 남겨야 하므로, 도메인별로
|
||||
리스너를 쪼개면 로그 스키마가 갈라질 위험이 커집니다. 반대로 카운트 동기화(`*CountSyncListener`)
|
||||
는 목적이 하나씩이라 도메인별로 쪼개져 있습니다 — 첨부 개수와 댓글 개수는 서로 독립적으로
|
||||
실패해도 되므로, 한쪽이 예외를 던져도 다른 쪽 동기화는 영향받지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 레이아웃 확장
|
||||
|
||||
<!-- @generated:layout-extensions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 대상 | 설명 |
|
||||
|---|---|
|
||||
| `resources/extensions/admin-ecommerce-inquiry-settings.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/admin_dashboard_community.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/admin_dashboard_quick_menu.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/user-notification-detail.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/user-notification-settings.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
<!-- @generated:layout-extensions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
5개 조각 중 `admin-ecommerce-inquiry-settings.json`·`admin_dashboard_community.json`·
|
||||
`admin_dashboard_quick_menu.json` 은 board 자신의 화면이 아니라 **다른 확장(이커머스 문의
|
||||
설정 화면, 관리자 대시보드)에** 게시판 관련 UI 를 끼워 넣는 조각입니다. 이 모듈이 다른 확장의
|
||||
레이아웃을 코드로 알지 못한 채(레이아웃 확장 시스템을 통해서만) UI 를 주입한다는 뜻입니다.
|
||||
나머지 2개(`user-notification-*`)는 코어 회원 알림 설정 화면에 board 알림 수신 옵션을
|
||||
끼워 넣는 자리입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 미들웨어
|
||||
|
||||
<!-- @generated:middleware START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 미들웨어가 없습니다._
|
||||
<!-- @generated:middleware END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
공개 API 라우트(`optional.sanctum`)와 관리자 API 라우트(코어 `auth`+권한 미들웨어)는 전부
|
||||
코어가 이미 등록한 미들웨어로 충분합니다. board 만의 요청 전처리(예: 게시판별 rate limit)가
|
||||
필요해지면 이 자리에 선언형으로 추가하되, 대상(targets)을 명시해 자기 라우트에만 부착합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 브로드캐스트 채널
|
||||
|
||||
<!-- @generated:channels START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 브로드캐스트 채널이 없습니다._
|
||||
<!-- @generated:channels END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
실시간 갱신(새 댓글이 열려 있는 화면에 즉시 반영되는 등)은 이 모듈의 범위 밖입니다(§1 참고).
|
||||
필요해지면 `sirsoft-board.{slug}.*` 채널을 신설하되, 게시판별로 채널을 분리해야 방문자가
|
||||
관심 없는 다른 게시판의 이벤트까지 구독하지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 스케줄
|
||||
|
||||
<!-- @generated:schedules START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 스케줄 | 주기 | 설명 |
|
||||
|---|---|---|
|
||||
| `sirsoft-board:aggregate-stats` | `-` | 대시보드 게시물 현황 집계 |
|
||||
| `sirsoft-board:prune-attachments --scheduled` | `-` | 방치된 임시 첨부 정리 + 보존기간 경과 삭제 첨부 영구 정리 |
|
||||
<!-- @generated:schedules END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`prune-attachments` 는 두 가지 서로 다른 작업을 한 스케줄에 묶습니다 — "방치된 임시 첨부
|
||||
정리"(업로드했지만 게시글 저장까지 이어지지 않은 파일)는 사용자 파일을 지우지 않으므로 항상
|
||||
실행되고, "보존기간 경과 삭제 첨부 영구 정리"(이미 삭제 처리된 첨부의 실제 파일 파기)는
|
||||
`attachment_settings.purge_enabled` 로 게이트됩니다 — module.php 의 `enabled_config: null` 은
|
||||
스케줄 자체는 끌 수 없다는 뜻이고, 실제 파기 여부만 설정으로 조정됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 알림 정의
|
||||
|
||||
<!-- @generated:notifications START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 알림 키 | 채널 |
|
||||
|---|---|
|
||||
| `new_comment` | `mail`, `database` |
|
||||
| `reply_comment` | `mail`, `database` |
|
||||
| `post_reply` | `mail`, `database` |
|
||||
| `post_action` | `mail`, `database` |
|
||||
| `new_post_admin` | `mail`, `database` |
|
||||
| `report_received_admin` | `mail`, `database` |
|
||||
| `report_action` | `mail`, `database` |
|
||||
<!-- @generated:notifications END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
7종 중 `new_comment`/`reply_comment`/`post_reply` 는 회원이 끌 수 있습니다
|
||||
(`UserNotificationSetting`, `core.user.*` 훅으로 회원 폼에 노출) — 반면 관리자 대상 알림
|
||||
(`new_post_admin`/`report_received_admin`)과 신고 처리 결과 알림(`post_action`/`report_action`)
|
||||
은 끌 수 없습니다. `post_action`과 `report_action`이 정확히 같은 훅 6개를 구독하는 것은
|
||||
중복이 아니라 **관점의 차이**입니다 — 관리자가 직접 블라인드했는지, 신고 처리 결과로
|
||||
블라인드됐는지에 따라 원 작성자에게 보이는 문구(원인 설명)가 갈라져야 하기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,121 @@
|
||||
# 게시판 — 프론트엔드
|
||||
|
||||
> 레이아웃·액션 핸들러·전역 진입점·에셋 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 레이아웃
|
||||
|
||||
<!-- @generated:layouts START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
레이아웃 46개 (루트: `resources/layouts`).
|
||||
|
||||
| 그룹 | 개수 |
|
||||
|---|---|
|
||||
| `admin` | 46개 |
|
||||
|
||||
| 레이아웃 | 그룹 | 종류 | extends |
|
||||
|---|---|---|---|
|
||||
| `admin_board_form` | `admin` | 화면 | `_admin_base` |
|
||||
| `admin_board_index` | `admin` | 화면 | `_admin_base` |
|
||||
| `admin_board_post_detail` | `admin` | 화면 | `_admin_base` |
|
||||
| `admin_board_post_form` | `admin` | 화면 | `_admin_base` |
|
||||
| `admin_board_posts_index` | `admin` | 화면 | `_admin_base` |
|
||||
| `admin_board_reports_detail` | `admin` | 화면 | `_admin_base` |
|
||||
| `admin_board_reports_index` | `admin` | 화면 | `_admin_base` |
|
||||
| `admin_board_settings` | `admin` | 화면 | `_admin_base` |
|
||||
| `_board_type_manage_modal` | `admin` | partial | - |
|
||||
| `_tab_basic` | `admin` | partial | - |
|
||||
| `_tab_list` | `admin` | partial | - |
|
||||
| `_tab_notification` | `admin` | partial | - |
|
||||
| `_tab_permissions` | `admin` | partial | - |
|
||||
| `_tab_post` | `admin` | partial | - |
|
||||
| `_comment` | `admin` | partial | - |
|
||||
| `_comments` | `admin` | partial | - |
|
||||
| `_post_card_content` | `admin` | partial | - |
|
||||
| `_reply_card_content` | `admin` | partial | - |
|
||||
| `_attachments` | `admin` | partial | - |
|
||||
| `_form_fields` | `admin` | partial | - |
|
||||
| `_parent_post` | `admin` | partial | - |
|
||||
| `_alert_status` | `admin` | partial | - |
|
||||
| `_card_history` | `admin` | partial | - |
|
||||
| `_card_report_info` | `admin` | partial | - |
|
||||
| `_bulk_apply_modal` | `admin` | partial | - |
|
||||
| `_modal_identity_policy_delete` | `admin` | partial | - |
|
||||
| `_modal_identity_policy_form` | `admin` | partial | - |
|
||||
| `_modal_mail_template_edit` | `admin` | partial | - |
|
||||
| `_modal_notification_definition_reset` | `admin` | partial | - |
|
||||
| `_modal_notification_template_edit` | `admin` | partial | - |
|
||||
| `_modal_notification_template_preview` | `admin` | partial | - |
|
||||
| `_tab_board_settings_attachment` | `admin` | partial | - |
|
||||
| `_tab_board_settings_basic` | `admin` | partial | - |
|
||||
| `_tab_board_settings_bulk_apply` | `admin` | partial | - |
|
||||
| `_tab_board_settings_comment` | `admin` | partial | - |
|
||||
| `_tab_board_settings_list` | `admin` | partial | - |
|
||||
| `_tab_board_settings_notification` | `admin` | partial | - |
|
||||
| `_tab_board_settings_permissions` | `admin` | partial | - |
|
||||
| `_tab_board_settings_post` | `admin` | partial | - |
|
||||
| `_tab_board_settings_reply` | `admin` | partial | - |
|
||||
| `_tab_general` | `admin` | partial | - |
|
||||
| `_tab_identity_policies` | `admin` | partial | - |
|
||||
| `_tab_notification_definitions` | `admin` | partial | - |
|
||||
| `_tab_report_policy` | `admin` | partial | - |
|
||||
| `_tab_seo` | `admin` | partial | - |
|
||||
| `_tab_spam_security` | `admin` | partial | - |
|
||||
<!-- @generated:layouts END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
46개 레이아웃이 **전부** `admin` 그룹입니다 — 이것은 이 모듈이 방문자 화면을 그리지 않는다는
|
||||
증거입니다(§1, §architecture.md 참고). 새로 방문자용 게시판 화면(예: 다른 스타일의 목록)이
|
||||
필요하면 이 디렉토리가 아니라 그 화면을 쓸 템플릿의 `layouts/` 에 추가합니다. `_tab_*` partial
|
||||
이 24개로 가장 많은 것은 이 모듈에 탭 구조를 쓰는 화면이 최소 세 곳이기 때문입니다 —
|
||||
게시판 생성/수정 폼(`_tab_basic`/`list`/`post`/`permissions`/`notification`), 게시판별
|
||||
개별 설정 화면(`_tab_board_settings_*` 9개 — attachment/basic/bulk_apply/comment/list/
|
||||
notification/permissions/post/reply), 모듈 전역 환경설정 화면(`_tab_general`/
|
||||
`identity_policies`/`notification_definitions`/`report_policy`/`seo`/`spam_security`).
|
||||
세 화면의 탭 이름이 겹치더라도(`_tab_basic`, `_tab_board_settings_basic` 등) 서로 다른
|
||||
partial 파일이므로 한쪽만 고치면 다른 화면은 그대로입니다 — 같은 항목을 여러 화면에
|
||||
반영해야 한다면 파일을 전부 찾아 고쳐야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 액션 핸들러
|
||||
|
||||
<!-- @generated:handlers START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 액션 핸들러가 없습니다._
|
||||
<!-- @generated:handlers END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
이 모듈의 관리자 레이아웃은 코어 빌트인 핸들러(`apiCall`/`navigate`/`setState` 등)만으로
|
||||
전부 구성됩니다 — 게시판 CRUD·신고 처리·설정 저장은 결국 REST 호출 + 표준 폼 상태 관리라
|
||||
전용 핸들러를 등록할 필요가 없었습니다. 새 관리자 화면을 추가할 때도 먼저 빌트인 핸들러
|
||||
조합으로 가능한지 확인하고, 그래도 부족할 때만(예: 파일 업로드 진행률 같은 복잡한 클라이언트
|
||||
상태) `resources/js/` 에 전용 핸들러를 신설합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 전역 진입점
|
||||
|
||||
<!-- @generated:frontend-entry START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_프론트 엔트리포인트가 없습니다._
|
||||
<!-- @generated:frontend-entry END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
액션 핸들러가 없는 것과 같은 이유로 `window.__[Name]` 재등록 진입점도 없습니다 — 로케일
|
||||
전환 후 재등록해야 할 자체 핸들러가 이 모듈에는 없기 때문입니다. 프론트 전용 코드
|
||||
(`resources/js/`)를 신설하면 그 순간부터 이 자리에 진입점을 만들어야 합니다(§CLAUDE.md
|
||||
"확장 미들웨어는..." 항목 인근의 재등록 진입점 규정 참고) — 없으면 로케일 전환 후 그 확장의
|
||||
액션이 전부 무반응이 됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 에셋
|
||||
|
||||
<!-- @generated:assets START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 구분 |
|
||||
|---|---|
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 (manifest) |
|
||||
|
||||
로딩 설정: `{"strategy":"global","priority":100,"dependencies":[]}`
|
||||
<!-- @generated:assets END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`editor-spec.json` 이 이 모듈의 유일한 프론트 자산인 것도 위와 같은 이유입니다 — 레이아웃
|
||||
편집기가 게시판 관리 화면의 컴포넌트를 인식하려면 이 선언이 필요하지만, 실행 시점에 로드할
|
||||
JS/CSS 번들은 없습니다. `priority: 100` 은 다른 확장의 에셋 우선순위와 충돌하지 않는 기본값이며,
|
||||
`dependencies: []` 는 이 확장의 에디터 스펙이 다른 확장의 스펙 로드를 전제하지 않는다는 뜻입니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,98 @@
|
||||
# 게시판 — 설정·권한·라우트
|
||||
|
||||
> 설정 스키마·권한·메뉴·라우트·의존 관계 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설정 스키마
|
||||
|
||||
<!-- @generated:settings-schema START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_`getSettingsSchema()` 선언이 없습니다._
|
||||
|
||||
기본값 파일: `config/settings/defaults.json`
|
||||
<!-- @generated:settings-schema END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`getSettingsSchema()` 가 없는 것은 누락이 아니라 설계입니다 — 이 모듈에는 "전역 설정"이라
|
||||
부를 만한 것이 없습니다. 운영자가 조정하는 값은 전부 **게시판 하나**에 속한 설정
|
||||
(`BoardSettingsService`, `/admin/boards/{slug}/settings`)이라 코어의 전역 설정 스키마
|
||||
메커니즘과 맞지 않습니다. `config/board.php` 는 운영자가 바꾸는 자리가 아니라 개발자가
|
||||
정의하는 상수(첨부 저장 디스크, 게시판별 동적 권한 정의 템플릿)이며, 이 값은 `.env` 로만
|
||||
바꿉니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 권한
|
||||
|
||||
<!-- @generated:permissions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 카테고리 | 이름 | 액션 | 라우트 키 |
|
||||
|---|---|---|---|
|
||||
| `boards` | 게시판 관리 | `read`, `create`, `update`, `delete` | `board` |
|
||||
| `settings` | 환경설정 | `read`, `update` | - |
|
||||
| `identity.policies` | 게시판 본인인증 정책 | `read`, `update` | - |
|
||||
| `dashboard` | 게시판 대시보드 | `view` | - |
|
||||
| `reports` | 게시판 신고 관리 | `view`, `manage` | `report` |
|
||||
<!-- @generated:permissions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
위 표는 **모듈 레벨** 권한(게시판 관리 자체를 다루는 관리자 권한)만 보여줍니다. 게시판 하나를
|
||||
만들면 그 게시판 전용 권한이 `config/board.php` 의 `board_permission_definitions` 템플릿을
|
||||
기반으로 추가 생성됩니다(admin.posts.read/write, posts.read-secret 등 — 게시판마다 독립적인
|
||||
권한 묶음). 그 동적 권한은 이 표에 나타나지 않으며 `getDynamicPermissionIdentifiers()` 로만
|
||||
전수를 확인할 수 있습니다. `boards`/`reports` 카테고리에 `resource_route_key`/`owner_key` 가
|
||||
붙어 있는 것은 소유자 기반 스코프 판정(자기 글만 관리 가능한 `manager` 이하 역할 등)이 걸려
|
||||
있다는 뜻입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 메뉴
|
||||
|
||||
<!-- @generated:menus START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 구분 | slug | 이름 | URL | 하위 |
|
||||
|---|---|---|---|---|
|
||||
| 관리자 | `sirsoft-board` | 게시판 관리 | - | 3개 |
|
||||
<!-- @generated:menus END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
정적 관리자 메뉴는 "게시판 관리" 3개 하위 메뉴(환경설정/목록/신고현황)뿐입니다. 게시판을
|
||||
만들 때마다 생기는 `board-{slug}` 메뉴는 동적 메뉴라 이 표에 없으며
|
||||
`getDynamicMenuSlugs()` 로 전수를 확인합니다. 방문자용 메뉴(사이트 상단 게시판 링크 등)는
|
||||
이 모듈이 등록하지 않습니다 — 템플릿이 공개 API(`boards.board-menu`)를 호출해 직접 구성합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 라우트
|
||||
|
||||
<!-- @generated:routes START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 파일 | URL prefix |
|
||||
|---|---|---|
|
||||
| `api` | `src/routes/api.php` | `/api/modules/sirsoft-board/...` |
|
||||
|
||||
확장 라우트는 **활성 상태인 확장의 것만** 등록됩니다. 라우트 정의를 바꾸면 라우트 캐시 재생성이 필요합니다.
|
||||
<!-- @generated:routes END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
같은 `src/routes/api.php` 파일 안에 관리자 전용 그룹(`/admin/board/{slug}/...`, 권한 미들웨어)과
|
||||
공개 그룹(`/boards/...`, `optional.sanctum`)이 함께 있습니다 — 파일을 분리하지 않은 것은
|
||||
board 의 라우트가 20개 안팎으로 한 파일에서 관리 가능한 규모이기 때문입니다. 새 공개
|
||||
엔드포인트를 추가할 때는 반드시 `optional.sanctum`(비회원도 접근 가능, 회원이면 컨텍스트
|
||||
주입)을 쓰고 `auth:sanctum` 을 쓰지 않습니다 — 게시판 열람은 비회원에게도 열려 있어야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 의존 관계
|
||||
|
||||
<!-- @generated:dependencies START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
없음 — 코어만으로 동작합니다.
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
| 확장 | 유형 | 요구 버전 |
|
||||
|---|---|---|
|
||||
| `sirsoft-basic` | 템플릿 | `>=1.0.0` |
|
||||
<!-- @generated:dependencies END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
이 모듈이 의존하는 확장이 "없음"인 것은 board 가 코어 훅·API 만으로 완결되도록 설계됐다는
|
||||
뜻입니다. 반대로 이 모듈에 의존하는 쪽은 하나(템플릿)뿐이지만, 그보다 결합이 느슨한
|
||||
**필터 훅 위임** 소비자(이커머스 문의)는 `dependencies` 로 선언되지 않습니다 — 이커머스는
|
||||
board 를 자기 도메인으로 대체할 뿐 board API 계약에 실제로 묶여 있지 않기 때문입니다.
|
||||
이 모듈의 공개 표면(라우트·API 응답 구조)을 바꿀 때는 `sirsoft-basic` 의 최소 버전 상향을
|
||||
검토해야 합니다(§CLAUDE.md "확장 → 확장 동기화").
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,184 @@
|
||||
# GDPR (일반 데이터 보호 규정) — 에이전트 가이드
|
||||
|
||||
> 이 문서는 이 플러그인을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 [README.md](README.md) 를 보세요.
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 유형: 플러그인 (sirsoft-gdpr) — 쿠키 동의 배너·동의 이력·GDPR/개인정보보호법 대응을 소유
|
||||
2. 확장 방식: `sirsoft-gdpr.consent.granted`/`revoked` 훅 구독, `data-gdpr-category` HTML 속성으로 자체 호스팅 자원 등록
|
||||
3. 건드리면 안 되는 것: `CookieConsentMiddleware`(functional 미동의 시 Set-Cookie 게이팅)의 strictly-necessary allowlist, 동의 이력(immutable append-only) 직접 수정
|
||||
4. 작업 위치: `plugins/_bundled/sirsoft-gdpr` — 활성 디렉토리 직접 수정 금지
|
||||
5. 반영: `php artisan plugin:update sirsoft-gdpr --force`
|
||||
```
|
||||
|
||||
## 1. 이 확장은 무엇인가
|
||||
|
||||
<!-- @intent START -->
|
||||
GDPR(EU) 및 한국 개인정보보호법이 요구하는 "동의 전 처리 금지" 원칙을 서버·클라이언트 양쪽에서
|
||||
강제하는 플러그인입니다. 두 계층이 서로 다른 것을 막습니다 — 서버 계층(`CookieConsentMiddleware`)
|
||||
은 백엔드가 심으려는 Set-Cookie 헤더를, 클라이언트 계층(자동 차단 스크립트)은 외부 추적
|
||||
스크립트·iframe·1st-party 저장소(localStorage/sessionStorage) 접근을 각각 동의 전까지 막습니다.
|
||||
"동의했다"는 사실 자체도 상태(`gdpr_user_consents`, mutable)와 이력(`gdpr_user_consent_histories`,
|
||||
immutable append-only)으로 이중 기록합니다 — 지금 상태 조회와 "언제 무엇에 동의했었는가" 입증
|
||||
(Art.7(1))은 서로 다른 질문이라 하나로 합칠 수 없습니다.
|
||||
|
||||
**설계 원칙**: 정책 버전 발행은 수동입니다 — 정책 본문이 바뀌었다고 자동으로 전 회원 재동의를
|
||||
트리거하지 않습니다. 운영자가 "이 변경이 재동의가 필요한 변경인가"를 판단해 명시적으로 발행
|
||||
버튼을 눌러야 합니다(README "사용 방법" 표 참고). 자동화하면 사소한 오탈자 수정에도 전 회원이
|
||||
재동의 화면을 보게 되어 UX 를 해칩니다.
|
||||
|
||||
**의도적으로 하지 않는 것**: 게스트 → 회원 동의 자동 승계(§README 소개 참고), 그리고 운영자가
|
||||
등록한 "허용" functional 쿠키 화이트리스트도 두지 않습니다 — functional 미동의 시 strictly
|
||||
necessary 4종(`XSRF-TOKEN`/세션/`laravel_maintenance`/`gdpr_session`)을 제외한 **모든** 쿠키를
|
||||
차단합니다. EDPB Guidelines 2/2023 §16 원칙이 "비필수는 동의 전 전면 차단"이지 "등록된 것만
|
||||
차단"이 아니기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 2. 디렉토리 지도
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Http/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Http/Resources/` | API 리소스 | 목록 응답은 화면이 실제로 그리는 것만 싣는다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Repositories/` | 데이터 접근 | 목록 쿼리는 컬럼 프루닝·정렬 화이트리스트 확인 |
|
||||
| `src/Models/` | Eloquent 모델 | 스키마 변경 시 마이그레이션 + 업그레이드 스텝 동반 |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/Enums/` | 상태·타입·분류 | 문자열 리터럴 대신 Enum 을 SSoT 로 둔다 |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `database/migrations/` | 마이그레이션 | 한국어 comment + `down()` 필수, 기설치본은 업그레이드 스텝으로 백필 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-gdpr --force` (빌드 불필요) |
|
||||
| `resources/routes.json` | 라우트 → 레이아웃 매핑 | `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
|
||||
## 3. 핵심 흐름
|
||||
|
||||
<!-- @intent START -->
|
||||
**동의 부여**: `Public\GdprCookieConsentController` → `GdprConsentService::grantConsent()` —
|
||||
회원이면 `user_id`, 게스트면 서명된 `gdpr_session` 쿠키로 식별한 뒤 `GdprUserConsent`(현재
|
||||
상태, upsert)와 `GdprUserConsentHistory`(이력, insert-only) 를 같은 트랜잭션에서 함께 기록하고
|
||||
`sirsoft-gdpr.consent.granted` 훅을 발행합니다. 이 흐름은 배너·마이페이지·회원가입·전체
|
||||
재동의 4개 진입점이 전부 공유합니다 — 진입점마다 다른 저장 로직을 만들지 않습니다.
|
||||
|
||||
**요청마다 반복되는 게이팅**: `CookieConsentMiddleware`(web/api 그룹에 prepend)가 응답 직전
|
||||
`GdprConsentService::getCurrentCookieConsents()` 로 현재 functional 동의 여부를 조회하고,
|
||||
미동의면 strictly-necessary 4종을 제외한 모든 Set-Cookie 헤더를 응답에서 제거합니다. 이
|
||||
서버측 게이팅과 별개로, 클라이언트에서는 `data-gdpr-category` 속성이 붙은 스크립트/iframe 과
|
||||
분석/마케팅 카테고리 도메인 매칭 리소스가 동의 전까지 로드되지 않습니다.
|
||||
|
||||
**회원탈퇴와 완전삭제는 다른 훅, 다른 처리**입니다 — `GdprUserWithdrawListener`
|
||||
(`core.user.after_withdraw`)는 코어가 user 행 자체를 보존하는 "탈퇴"에 반응해 활성 동의를
|
||||
전부 철회 처리(UPDATE + `source=withdraw` revoked 이력 INSERT)할 뿐 신원 정보는 그대로
|
||||
남깁니다 — 탈퇴는 "의사 표시 종료"이지 신원 삭제가 아니기 때문입니다. 반대로
|
||||
`GdprUserDeleteListener`(`core.user.before_delete`, 완전 삭제/hard delete)는 이력 행의
|
||||
`user_id`/IP/User-Agent 만 NULL 로 **익명화**하고 행 자체는 남깁니다(Art.17 삭제권과 Art.7(1)
|
||||
입증 의무 양립). 두 훅을 헷갈리면 탈퇴 시점에 신원이 조기 삭제되거나, 완전삭제 시점에 입증
|
||||
자료 행까지 통째로 사라집니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 4. 확장점
|
||||
|
||||
<!-- @generated:extension-points-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 확장점 | 수 | 상세 |
|
||||
|---|---|---|
|
||||
| 발행 훅 | 2개 | [발행 훅](docs/extension-points.md#발행-훅) |
|
||||
| 구독 훅 | 4개 | [구독 훅](docs/extension-points.md#구독-훅) |
|
||||
| 훅 리스너 | 4개 | [훅 리스너](docs/extension-points.md#훅-리스너) |
|
||||
| 레이아웃 확장 | 2개 | [레이아웃 확장](docs/extension-points.md#레이아웃-확장) |
|
||||
| 미들웨어 | 1개 | [미들웨어](docs/extension-points.md#미들웨어) |
|
||||
| 브로드캐스트 채널 | 0개 | [브로드캐스트 채널](docs/extension-points.md#브로드캐스트-채널) |
|
||||
| 스케줄 | 0개 | [스케줄](docs/extension-points.md#스케줄) |
|
||||
| 알림 정의 | 0개 | [알림 정의](docs/extension-points.md#알림-정의) |
|
||||
<!-- @generated:extension-points-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
다른 확장이 이 플러그인 없이는 몰랐을 사실(방문자가 방금 분석 카테고리에 동의/철회했다)을
|
||||
알아야 할 때 `sirsoft-gdpr.consent.granted`/`revoked` 훅을 잡습니다 — 예: 분석 SDK 초기화를
|
||||
"페이지 로드 시 무조건"이 아니라 "동의 부여 시에만" 하고 싶은 확장. 반대로 자체 호스팅
|
||||
추적 자원을 이 플러그인의 자동 차단·복원 대상에 포함시키고 싶다면 훅이 아니라 HTML 속성
|
||||
(`data-gdpr-category="analytics"`)을 붙이는 쪽이 맞습니다 — 이 플러그인이 그 속성을 스캔해
|
||||
차단/복원을 대신 수행하므로 소비 측 코드가 필요 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 5. 수정 시 동반 의무
|
||||
|
||||
- [ ] `_bundled` 에서만 수정하고 `php artisan plugin:update sirsoft-gdpr --force` 로 반영
|
||||
- [ ] manifest version 상향 시 `package.json` · `package-lock.json` · `composer.json` 동기화 + CHANGELOG 기재
|
||||
- [ ] 스키마 변경 시 마이그레이션(한국어 comment + `down()`) + 기설치본 백필용 업그레이드 스텝
|
||||
- [ ] 발행 훅 추가·이름 변경 시 `php artisan ext:docgen` 재실행 (구독하는 확장의 계약이 바뀝니다)
|
||||
- [ ] API 표면 변경 시 `php artisan api:docgen --scope=plugin:sirsoft-gdpr` 재실행 + `docs/api/**` 갱신
|
||||
- [ ] 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
|
||||
- [ ] TSX/TS 변경 시 `--production` 재빌드 후 `dist/` 커밋 (sourceMappingURL 잔존 금지)
|
||||
- [ ] 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
|
||||
- [ ] `gdpr_user_consent_histories` 는 append-only — UPDATE/DELETE 로 기존 행을 고치지 않는다 (완전삭제 시 익명화 UPDATE 예외는 `GdprUserDeleteListener` 단일 지점에서만 수행)
|
||||
- [ ] 새 자동 차단 카테고리(기능/분석/마케팅 외)를 추가하면 배너 UI·`blocked_domains` 스키마·차단 스크립트 3곳 동기화
|
||||
- [ ] `CookieConsentMiddleware` 의 strictly-necessary allowlist(4종)를 확장할 때는 ePrivacy Art.5(3) 면제 항목인지 먼저 검토 — 임의로 늘리면 동의 전 차단 원칙이 무력화된다
|
||||
|
||||
## 6. 금지 패턴
|
||||
|
||||
<!-- @intent START -->
|
||||
| 금지 | 올바른 사용 | 이유 |
|
||||
|---|---|---|
|
||||
| `gdpr_user_consent_histories` 행을 UPDATE/DELETE 로 직접 정정 | 정정이 필요하면 새 이력 행을 INSERT | 이력은 시점별 스냅샷이 생명 — 과거 행을 고치면 Art.7(1) 입증 자료로서 효력을 잃는다 |
|
||||
| 회원탈퇴(`after_withdraw`)에서 신원 정보(user_id 등)를 제거 | 활성 동의만 철회 처리, 신원은 완전삭제(`before_delete`) 시점에만 익명화 | 두 이벤트를 섞으면 탈퇴 회원의 재가입·이력 조회가 깨진다 |
|
||||
| 운영자가 등록하지 않은 functional 쿠키를 화이트리스트에 추가 | strictly necessary 4종 고정 목록만 예외 | GDPR 은 "동의 전 전면 차단"이 원칙이지 "등록된 것만 차단"이 아니다 |
|
||||
| 정책 버전 발행을 코드/배치로 자동화 | 운영자가 매번 명시적으로 "+ 새 버전 발행" 클릭 | 자동화하면 사소한 문구 수정에도 전 회원이 재동의 화면을 보게 된다 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 7. 테스트 실행
|
||||
|
||||
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 개수 | 위치 |
|
||||
|---|---|---|
|
||||
| PHPUnit | 24개 | `plugins/_bundled/sirsoft-gdpr/tests` |
|
||||
| Vitest | 12개 | `vitest.config.ts` |
|
||||
| Playwright | 3개 | `tests/Playwright` |
|
||||
| 시나리오 매니페스트 | 5개 | `tests/scenarios` |
|
||||
|
||||
기저 TestCase: `tests/PluginTestCase.php` — 확장 테스트는 이 클래스를 상속합니다 (`Tests\TestCase` 직접 상속 금지).
|
||||
|
||||
```bash
|
||||
# PHPUnit (변경 범위만) (Bash)
|
||||
php vendor/bin/phpunit plugins/_bundled/sirsoft-gdpr/tests --filter='<대상클래스>'
|
||||
|
||||
# Vitest (확장 디렉토리에서) (PowerShell)
|
||||
cd plugins/_bundled/sirsoft-gdpr && powershell -Command "npm run test:run -- <대상>"
|
||||
|
||||
# Playwright E2E (Bash)
|
||||
npx playwright test plugins/_bundled/sirsoft-gdpr/tests/Playwright/specs/<대상>.spec.ts
|
||||
|
||||
```
|
||||
|
||||
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
|
||||
<!-- @generated:test-commands END -->
|
||||
|
||||
## 8. 문서 목차
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
@@ -4,6 +4,12 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.4] - 2026-08-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- 회원탈퇴로 자동 철회된 동의 이력이 관리자 「GDPR 동의 이력」 화면의 출처 필터로 걸러지지 않던 문제를 수정했습니다.
|
||||
|
||||
## [1.0.3] - 2026-08-19
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,151 +1,209 @@
|
||||
# GDPR Plugin for G7
|
||||
# GDPR
|
||||
|
||||
GDPR(유럽 일반 데이터 보호 규정) 및 한국 개인정보보호법 대응 핵심 기능을 제공하는 G7 플러그인입니다. 쿠키 동의 배너·동의 전 자동 차단·동의 이력 영구 저장·마이페이지 동의 철회를 한 패키지로 제공합니다.
|
||||
**G7 플러그인 · sirsoft-gdpr**
|
||||
GDPR·개인정보보호법 대응 쿠키 동의 배너와 동의 이력 관리를 제공하는 플러그인
|
||||
|
||||
## 핵심 기능
|
||||
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-1.0.4-0066FF?style=flat-square" alt="version 1.0.4">
|
||||
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
|
||||
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.6-1F883D?style=flat-square" alt="G7 >=7.0.6">
|
||||
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
|
||||
</p>
|
||||
<!-- @generated:badges END -->
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 쿠키 동의 배너 | 필수/기능/분석/마케팅 4분류 (ICO·CNIL 권장 표준), 다크 모드, 4 위치(하단 바·좌하단·우하단·중앙 모달) |
|
||||
| 동의 전 자동 차단 | 외부 추적 스크립트·iframe + 기능 카테고리 1st-party 저장소(localStorage·sessionStorage·1st-party 쿠키) 게이팅 |
|
||||
| 동의 이력 저장 | 정책 버전·출처·카테고리 스냅샷을 함께 immutable 보존 (GDPR Art.7(1) 입증 자료) |
|
||||
---
|
||||
|
||||
[소개](#소개) · [주요 기능](#주요-기능) · [동작 방식](#동작-방식) · [요구 사항](#요구-사항) · [설치](#설치) · [관리자 설정](#관리자-설정) · [사용 방법](#사용-방법) · [다른 확장과의 연동](#다른-확장과의-연동) · [문서](#문서) · [트러블슈팅](#트러블슈팅) · [변경 이력](#변경-이력) · [라이선스](#라이선스)
|
||||
|
||||
---
|
||||
|
||||
## 소개
|
||||
|
||||
<!-- @intent START -->
|
||||
GDPR(유럽 일반 데이터 보호 규정) 및 한국 개인정보보호법 대응 핵심 기능을 제공하는 플러그인입니다.
|
||||
쿠키 동의 배너·동의 전 자동 차단·동의 이력 영구 보존·마이페이지 동의 철회를 한 패키지로
|
||||
제공합니다.
|
||||
|
||||
이 플러그인이 의도적으로 하지 않는 것 하나는 **게스트 → 회원 동의 자동 승계**입니다. GDPR
|
||||
Art.6/ePrivacy Art.5(3) 관점에서 게스트(디바이스 단위)와 회원(주체 단위)은 별도 동의 모델이며,
|
||||
회원가입 폼 동의로 Art.7(1) 입증 책임이 별도로 충족됩니다. 게스트 시절 동의 이력은 세션 기준
|
||||
으로 보존될 뿐 회원 계정과 자동으로 이어붙지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 주요 기능
|
||||
|
||||
<!-- @intent START -->
|
||||
| 영역 | 설명 |
|
||||
|---|---|
|
||||
| 쿠키 동의 배너 | 필수/기능/분석/마케팅 4분류(ICO·CNIL 권장 표준), 다크 모드, 4가지 위치(하단 바·좌하단·우하단·중앙 모달) |
|
||||
| 동의 전 자동 차단 | 외부 추적 스크립트·iframe + 기능 카테고리 1st-party 저장소(localStorage·sessionStorage·쿠키) 게이팅 |
|
||||
| 동의 이력 저장 | 정책 버전·출처·카테고리 스냅샷을 함께 immutable 보존(GDPR Art.7(1) 입증 자료) |
|
||||
| 마이페이지 동의 관리 | 회원이 자신의 동의 현황을 조회·개별 철회·재동의·전체 일괄 재동의 |
|
||||
| 관리자 동의 이력 조회 | 회원/게스트 동의 변경 이력 (이메일·세션 검색, 카테고리·출처 다중 필터, 카테고리 스냅샷 표) |
|
||||
| 정책 버전 수동 발행 | 정책 본문 변경 시 「+ 새 버전 발행」 클릭으로 모든 회원 재동의 트리거 |
|
||||
| 관리자 동의 이력 조회 | 회원/게스트 동의 변경 이력(이메일·세션 검색, 카테고리·출처 다중 필터, 카테고리 스냅샷 표) |
|
||||
| 정책 버전 발행 | 정책 본문 변경 시 수동 발행으로 모든 회원에게 재동의를 트리거 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 동작 방식
|
||||
|
||||
<!-- @intent START -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
V[방문자] -->|첫 방문| Banner[쿠키 배너 노출]
|
||||
Banner -->|동의 전| Block[외부 스크립트·iframe·1st-party 저장소 자동 차단]
|
||||
Banner -->|동의| Grant[GdprUserConsent 저장 + 훅 발행]
|
||||
Grant --> Restore[차단 해제·스크립트 로드]
|
||||
Grant --> History[(동의 이력 append-only 보존)]
|
||||
|
||||
Admin[운영자] -->|정책 본문 변경| Publish[새 정책 버전 발행]
|
||||
Publish -->|다음 방문| Renew[모든 회원 재동의 안내]
|
||||
```
|
||||
|
||||
동의는 부여든 철회든 상태 저장(`gdpr_user_consents`, mutable)과 이력 기록
|
||||
(`gdpr_user_consent_histories`, immutable append-only)이 항상 함께 일어납니다 — "지금 동의
|
||||
상태가 무엇인가"와 "언제 무엇에 동의했었는가"를 구분해서 답할 수 있어야 하기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 요구 사항
|
||||
|
||||
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| G7 코어 | `>=7.0.6` |
|
||||
| PHP | `^8.2` |
|
||||
<!-- @generated:requirements END -->
|
||||
|
||||
## 설치
|
||||
|
||||
<!-- @generated:install START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
```bash
|
||||
# 번들 설치 (코어에 동봉된 소스에서 설치)
|
||||
php artisan plugin:install sirsoft-gdpr
|
||||
|
||||
# 활성화
|
||||
php artisan plugin:activate sirsoft-gdpr
|
||||
|
||||
# 업데이트 (번들 소스 기준 강제 반영)
|
||||
php artisan plugin:update sirsoft-gdpr --force
|
||||
```
|
||||
|
||||
설치 직후 쿠키 배너는 **비활성** 상태로 제공됩니다. 운영자가 운영 주체명·데이터 저장 위치·정책 페이지 슬러그를 입력한 뒤 「쿠키 배너 노출」 토글을 켜야 사이트에 노출됩니다.
|
||||
저장소: https://github.com/gnuboard/g7-plugin-sirsoft-gdpr
|
||||
<!-- @generated:install END -->
|
||||
|
||||
## 설정
|
||||
설치 직후 쿠키 배너는 **비활성** 상태입니다. 운영자가 운영 주체명·데이터 저장 위치·정책 페이지
|
||||
슬러그를 입력한 뒤 "쿠키 배너 노출" 토글을 켜야 사이트에 노출됩니다.
|
||||
|
||||
`관리자 → 플러그인 → GDPR 설정` 에서 구성합니다.
|
||||
## 관리자 설정
|
||||
|
||||
### 운영 정보
|
||||
<!-- @generated:settings-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 의미 | 기본값 |
|
||||
|---|---|---|
|
||||
| `privacy_policy_slug` | 개인정보처리방침 페이지 슬러그 | `privacy` |
|
||||
| `legal_entity_name` | 운영 주체명 | - |
|
||||
| `data_storage_location` | 데이터 저장 위치 | - |
|
||||
| `banner_enabled` | 쿠키 배너 노출 | `true` |
|
||||
| `banner_position` | 배너 위치 | `bottom_bar` |
|
||||
| `blocked_domains` | 추적 도메인 차단 목록 | `{"functional":["*.crisp.chat","client.crisp.chat","*.intercom.io","widget.intercom.io","*.tawk.to","embed.tawk.to","cdn.weglot.com","*.weglot.com","*.usercentrics.eu"],"analytics":["google-analytics.com","*.google-analytics.com","googletagmanager.com","*.googletagmanager.com","ssl.google-analytics.com","*.hotjar.com","static.hotjar.com","*.mixpanel.com","cdn.mxpnl.com","*.amplitude.com","cdn.amplitude.com","*.segment.io","*.segment.com","wcs.naver.net","wcs.naver.com","*.beusable.net"],"marketing":["facebook.net","connect.facebook.net","facebook.com","*.facebook.com","doubleclick.net","*.doubleclick.net","googleadservices.com","googlesyndication.com","ads.google.com","*.criteo.com","static.criteo.net","*.adnxs.com","*.taboola.com","cdn.taboola.com","*.outbrain.com","*.kakao.com","analytics.ad.daum.net","platform.twitter.com","*.twitter.com","platform.linkedin.com","*.linkedin.com"]}` |
|
||||
| `cookie_categories` | 쿠키 카테고리 정의 | `[]` |
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| 운영 주체명 (`legal_entity_name`) | 쿠키 배너 푸터·마이페이지 동의 카드에 노출되는 사이트 운영 주체 (예: "(주)홍길동컴퍼니") |
|
||||
| 데이터 저장 위치 (`data_storage_location`) | 사용자에게 안내할 데이터 저장 국가 (예: "대한민국", "미국 (AWS)"). IP 주소·CIDR·클라우드 리전 코드(예: `ap-northeast-2`)는 보안상 자동 거부됩니다 |
|
||||
| 개인정보처리방침 슬러그 (`privacy_policy_slug`) | sirsoft-page 플러그인에 등록된 처리방침 페이지의 슬러그. 비어있으면 쿠키 배너의 정책 링크가 자동 숨겨집니다 |
|
||||
개발자용 상세(타입·검증·저장 위치)는 [설정 스키마](docs/settings.md#설정-스키마) 를 보세요.
|
||||
<!-- @generated:settings-summary END -->
|
||||
|
||||
### 쿠키 배너
|
||||
<!-- @intent START -->
|
||||
`쿠키 배너 노출` 은 마스터 토글입니다 — ON 하면 배너와 자동 차단이 **함께** 켜집니다. GDPR
|
||||
Art.6 "동의 전 처리 금지"를 강제하는 메커니즘인 자동 차단만 단독으로 끌 수 없도록 의도적으로
|
||||
하나의 토글에 묶었습니다. 반대로 마이페이지 동의 관리 카드는 이 토글과 무관하게, 동의/철회
|
||||
이력이 있는 회원에게는 항상 노출됩니다(Art.7(3) 철회 대칭성 보장 — 동의를 배너로 받았다면
|
||||
철회도 언제든 마이페이지에서 가능해야 합니다).
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| 쿠키 배너 노출 (`banner_enabled`) | 마스터 토글 — ON 시 배너 + 자동 차단이 함께 활성화됩니다. GDPR Art.6 "동의 전 처리 금지" 의 강제 메커니즘인 자동 차단을 단독 OFF 할 수 없도록 단일 토글로 통합되어 있습니다. 마이페이지 동의 관리 카드는 이 토글과 무관하게 동의/철회 이력이 있는 회원에게 항상 노출됩니다 (Art.7(3) 철회 대칭성 보장) |
|
||||
| 배너 위치 (`banner_position`) | 하단 바 / 좌하단 팝업 / 우하단 팝업 / 중앙 모달 |
|
||||
데이터 저장 위치(`data_storage_location`)에는 IP 주소·CIDR·클라우드 리전 코드(예:
|
||||
`ap-northeast-2`)를 입력할 수 없습니다 — 보안상 자동 거부됩니다. "대한민국", "미국 (AWS)"처럼
|
||||
사용자에게 안내할 국가/지역명만 입력합니다.
|
||||
|
||||
### 자동 차단 정책
|
||||
자동 차단 대상 도메인은 카테고리(기능/분석/마케팅)별 카탈로그로 관리하며, 기본 카탈로그(예:
|
||||
Google Analytics, Facebook Pixel, Kakao Pixel 등)가 시드되어 있고 운영자가 추가·삭제할 수
|
||||
있습니다. 도메인 형식은 `example.com` 또는 와일드카드 `*.example.com` 만 지원하며, `localhost`
|
||||
같은 단일 라벨과 한글 도메인(xn-- 변환)은 지원하지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
쿠키 배너가 ON 일 때 다음 카테고리의 외부 도메인 리소스가 동의 전까지 자동 차단됩니다. 카탈로그 기본값이 시드되어 있으며, 운영자가 카테고리별로 추가·삭제할 수 있습니다.
|
||||
## 사용 방법
|
||||
|
||||
| 카테고리 | 기본 카탈로그 예시 |
|
||||
|---------|------------------|
|
||||
| 기능 (functional) | Crisp, Intercom, Tawk.to, Weglot 등 |
|
||||
| 분석 (analytics) | Google Analytics, Hotjar, Mixpanel, 네이버 프리미엄 로그분석 등 |
|
||||
| 마케팅 (marketing) | Facebook Pixel, Google Ads, Kakao Pixel, YouTube embed 등 |
|
||||
|
||||
도메인 형식: `example.com` 또는 와일드카드 `*.example.com`. `localhost` 같은 단일 라벨, 한글 도메인(xn-- 변환)은 미지원.
|
||||
|
||||
## 자체 호스팅 추적 자원 분류
|
||||
|
||||
자체 도메인에서 호스팅되는 추적 스크립트·iframe·임베드는 도메인 매칭 대상이 아니므로 HTML 속성으로 분류합니다.
|
||||
<!-- @intent START -->
|
||||
**자체 호스팅 추적 자원 등록**: 자체 도메인에서 서빙하는 추적 스크립트·iframe·임베드는 도메인
|
||||
매칭 대상이 아니므로 HTML 속성으로 분류합니다.
|
||||
|
||||
```html
|
||||
<script src="/js/my-analytics.js" data-gdpr-category="analytics"></script>
|
||||
<iframe src="/embed/custom-tracker" data-gdpr-category="marketing"></iframe>
|
||||
```
|
||||
|
||||
동의 전까지 자동 차단되며, 동의 후 자동 복원됩니다. 동의 철회 시 다시 차단됩니다.
|
||||
동의 전까지 자동 차단되고, 동의 후 자동 복원되며, 동의 철회 시 다시 차단됩니다.
|
||||
|
||||
## 정책 버전 발행
|
||||
|
||||
`관리자 → 플러그인 → GDPR 설정` 의 「정책 버전」 카드에서 「+ 새 버전 발행」 을 클릭합니다. 발행 즉시 모든 회원이 다음 방문 시 재동의 화면(amber 안내 박스 + 「최신 정책으로 갱신」 버튼)을 보게 됩니다.
|
||||
**정책 버전 발행**: `관리자 → 플러그인 → GDPR 설정` 의 "정책 버전" 카드에서 "+ 새 버전 발행"을
|
||||
누르면 모든 회원이 다음 방문 시 재동의 화면을 보게 됩니다. 아래 기준으로 발행 여부를 판단합니다.
|
||||
|
||||
| 발행이 필요한 변경 | 발행이 필요 없는 변경 |
|
||||
|------------------|---------------------|
|
||||
| 정책 본문 (개인정보처리방침 페이지) 변경 | 차단 도메인 추가/삭제 |
|
||||
|---|---|
|
||||
| 정책 본문(개인정보처리방침 페이지) 변경 | 차단 도메인 추가/삭제 |
|
||||
| 카테고리 의미 변경 | UI 라벨/설명 정정 |
|
||||
| 위탁자·데이터 보관 정보 변경 | 운영 주체명·저장 위치 정정 |
|
||||
|
||||
발행 시 변경 사유 메모를 함께 저장합니다 (GDPR Art.30 처리 기록 의무).
|
||||
발행 시 변경 사유 메모를 함께 저장합니다(GDPR Art.30 처리 기록 의무).
|
||||
|
||||
## 동의 이력 조회
|
||||
**동의 이력 조회**: `관리자 → GDPR 동의 이력` 메뉴에서 이메일 부분 일치·세션 ID 로 검색하고,
|
||||
카테고리·출처(banner/mypage/register/withdraw 등)·동의 액션(granted/revoked)으로 필터링합니다.
|
||||
각 행을 펼치면 동의 시점의 전체 카테고리 의사 스냅샷을 확인할 수 있습니다(Art.7(1) 입증 자료).
|
||||
<!-- @intent END -->
|
||||
|
||||
`관리자 → GDPR 동의 이력` 메뉴에서 회원/게스트의 모든 동의 변경 이력을 조회할 수 있습니다.
|
||||
## 다른 확장과의 연동
|
||||
|
||||
- **검색**: 이메일 부분 일치, 세션 ID
|
||||
- **필터**: 카테고리(필수/기능/분석/마케팅), 출처(banner/mypage/register/withdraw 등), 동의 액션(granted/revoked)
|
||||
- **카테고리 스냅샷**: 각 행 펼침에서 동의 시점의 전체 카테고리 의사 표를 immutable 보존 (GDPR Art.7(1) 입증 자료)
|
||||
<!-- @generated:integrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
## 가용 훅 (Hook)
|
||||
없음 — 코어만으로 동작합니다.
|
||||
|
||||
다른 확장에서 본 플러그인의 동의 이벤트를 구독할 수 있습니다.
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
### 액션 훅
|
||||
없음.
|
||||
<!-- @generated:integrations END -->
|
||||
|
||||
| 훅 이름 | 시점 | 인수 |
|
||||
|---------|------|------|
|
||||
| `sirsoft-gdpr.consent.granted` | 동의 부여 시 | `GdprUserConsent $consent, string $source` |
|
||||
| `sirsoft-gdpr.consent.revoked` | 동의 철회 시 | `GdprUserConsent $consent, string $source` |
|
||||
<!-- @intent START -->
|
||||
`sirsoft-page` 는 소프트 의존(런타임 체크)입니다 — 미설치 시 쿠키 배너의 "자세히" 정책 링크만
|
||||
자동으로 숨겨지고 나머지 기능은 정상 동작합니다. `sirsoft-basic` 템플릿은 배너가 주입되는
|
||||
지점(`_user_base.json` 의 공용 확장 지점)을 제공해야 하므로, 다른 사용자 템플릿을 쓰려면
|
||||
그 템플릿에도 같은 주입 지점이 있어야 배너가 정상 노출됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
`$source` 값: `banner` / `mypage` / `mypage_renew_all` / `register` / `withdraw`.
|
||||
## 문서
|
||||
|
||||
### 훅 등록 예시
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
|
||||
```php
|
||||
use App\Extension\HookManager;
|
||||
## 트러블슈팅
|
||||
|
||||
HookManager::addAction(
|
||||
'sirsoft-gdpr.consent.granted',
|
||||
function ($consent, string $source) {
|
||||
// 예: 분석 동의 부여 시 외부 분석 도구에 사용자 식별 전송
|
||||
if ($consent->consent_key === 'cookie_analytics' && $consent->is_consented) {
|
||||
AnalyticsService::identify($consent->user_id);
|
||||
}
|
||||
},
|
||||
priority: 10
|
||||
);
|
||||
```
|
||||
<!-- @intent START -->
|
||||
| 증상 | 원인 | 조치 |
|
||||
|---|---|---|
|
||||
| 쿠키 배너 설정을 저장했는데도 배너가 안 뜸 | "쿠키 배너 노출" 마스터 토글이 꺼져 있음 | 관리자 설정에서 토글을 켠다 — 개별 항목 저장만으로는 배너가 켜지지 않는다 |
|
||||
| 정책 페이지 링크가 배너에 안 보임 | `privacy_policy_slug` 미설정 또는 `sirsoft-page` 미설치 | 슬러그를 입력하거나, 링크 없이 운영할지 결정한다(자동 숨김은 정상 동작) |
|
||||
| 배너에서 동의했는데 마이페이지에 이력이 안 보임 | 게스트 상태에서 동의 후 회원가입한 경우 — 게스트→회원 자동 승계를 제공하지 않음 | 의도된 동작이다(§소개 참고). 회원가입 시 별도 동의를 다시 받는다 |
|
||||
| 자체 호스팅 스크립트가 동의 전에도 로드됨 | `data-gdpr-category` 속성 누락 | 스크립트/iframe 태그에 카테고리 속성을 추가한다 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 데이터베이스
|
||||
## 변경 이력
|
||||
|
||||
| 테이블 | 용도 | 보존 정책 |
|
||||
|--------|------|----------|
|
||||
| `gdpr_user_consents` | 회원 현재 동의 상태 (mutable) | 사용자 삭제 시 명시 삭제 |
|
||||
| `gdpr_user_consent_histories` | 동의 변경 이력 (immutable append-only) | 사용자 삭제 시 `user_id`/IP/UA 만 NULL 익명화하여 행 보존 (GDPR Art.17 + Art.7(1) 양립) |
|
||||
| `gdpr_policy_versions` | 정책 버전 발행 이력 (불변) | 영구 보존 (Art.30 처리 기록) |
|
||||
|
||||
플러그인 제거 시 위 3 테이블이 자동 DROP 됩니다.
|
||||
|
||||
## 게스트 → 회원 동의 승계
|
||||
|
||||
본 플러그인은 게스트 → 회원 동의 자동 승계를 제공하지 않습니다. GDPR Art.6/ePrivacy Art.5(3) 관점에서 게스트(디바이스 단위)와 회원(주체 단위)은 별도 동의 모델이며, 글로벌 CMP 대부분도 자동 승계를 기본으로 제공하지 않습니다. 회원가입 폼 동의로 Art.7(1) 입증 책임이 충족되며, 게스트 시절 동의 이력은 세션 기준으로 보존됩니다.
|
||||
|
||||
## 의존성
|
||||
|
||||
| 대상 | 의존 수준 | 미설치 시 동작 |
|
||||
|------|----------|---------------|
|
||||
| `sirsoft-page` 모듈 | 소프트 (런타임 체크) | 배너 "자세히" 링크만 자동 숨김. 나머지 정상 |
|
||||
| `sirsoft-basic` 템플릿 | 주입 지점 의존 | 다른 사용자 템플릿 사용 시 해당 템플릿에도 `_user_base.json` 의 공용 확장 지점이 있어야 정상 동작 |
|
||||
|
||||
## 테스트 실행
|
||||
|
||||
```bash
|
||||
# 백엔드
|
||||
php vendor/bin/phpunit plugins/_bundled/sirsoft-gdpr/tests
|
||||
|
||||
# 프론트엔드
|
||||
cd plugins/_bundled/sirsoft-gdpr
|
||||
npm run test:run
|
||||
```
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT
|
||||
MIT
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "plugins/sirsoft-gdpr",
|
||||
"description": "GDPR (General Data Protection Regulation) plugin for G7 platform by sirsoft",
|
||||
"type": "library",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# GDPR (일반 데이터 보호 규정) 개발자 문서
|
||||
|
||||
> plugins/_bundled/sirsoft-gdpr · 플러그인
|
||||
|
||||
<!-- @generated:stats START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**훅 수**: 2 · **구독 훅 수**: 4 · **라우트 수**: 15 · **모델 수**: 3 · **테이블 수**: 3 · **마이그레이션 수**: 4 · **레이아웃 수**: 4 · **핸들러 수**: 1
|
||||
<!-- @generated:stats END -->
|
||||
|
||||
## 문서 목차
|
||||
|
||||
<!-- @generated:doc-toc START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | 설계 의도·계층 지도·디렉토리 맵 |
|
||||
| [extension-points.md](extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 |
|
||||
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
|
||||
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
|
||||
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
|
||||
| [api/](api/README.md) | API 레퍼런스 |
|
||||
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
|
||||
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
|
||||
<!-- @generated:doc-toc END -->
|
||||
@@ -0,0 +1,78 @@
|
||||
# GDPR (일반 데이터 보호 규정) — 아키텍처
|
||||
|
||||
> 설계 의도와 계층 구조 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설계 의도
|
||||
|
||||
<!-- @intent START -->
|
||||
"동의 전 처리 금지"를 소스 하나가 아니라 **서버(쿠키)·클라이언트(스크립트/저장소) 두 표면
|
||||
모두**에서 강제하는 것이 이 플러그인의 핵심 설계입니다. 서버 표면만 막으면 클라이언트에서
|
||||
직접 실행되는 추적 스크립트를 막지 못하고, 클라이언트 표면만 막으면 백엔드가 심는 분석용
|
||||
쿠키를 막지 못합니다. 두 표면은 서로 다른 코드 경로(미들웨어 vs 프론트 차단 로직)이지만
|
||||
같은 동의 판정 소스(`GdprConsentService`)를 공유해야 판정이 갈리지 않습니다.
|
||||
|
||||
동의 "상태"와 "이력"을 분리 보존하는 것도 설계 결정입니다 — 상태(mutable)는 지금 게이팅
|
||||
판정에 쓰이고, 이력(immutable append-only)은 감사 대응(Art.7(1))에 쓰입니다. 회원탈퇴·완전삭제
|
||||
두 이벤트에서 서로 다른 처리(철회 vs 익명화)를 하는 것도 이 분리 때문에 가능합니다 — 하나의
|
||||
테이블이었다면 "지금 상태를 지울까 이력을 지울까"를 매번 다시 판단해야 했을 것입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 계층 지도
|
||||
|
||||
<!-- @intent START -->
|
||||
```
|
||||
Http/Controllers (Admin/ 관리자 설정·동의이력·정책버전, Public/ 배너 API, User/ 마이페이지)
|
||||
│
|
||||
▼
|
||||
Services (GdprConsentService/GdprSettingsService/GdprPolicyVersionService/
|
||||
GdprConsentLogService/CookieCategoryService)
|
||||
│
|
||||
├──▶ Models (GdprUserConsent 상태 · GdprUserConsentHistory 이력 · GdprPolicyVersion)
|
||||
│
|
||||
└──▶ 훅 발행 (consent.granted/revoked) ──▶ 다른 확장 리스너
|
||||
|
||||
CookieConsentMiddleware (web/api 그룹 prepend)
|
||||
│
|
||||
└──▶ GdprConsentService::getCurrentCookieConsents() 조회 후 응답 Set-Cookie 게이팅
|
||||
|
||||
core.user.after_withdraw / before_delete 훅
|
||||
│
|
||||
└──▶ GdprUserWithdrawListener(철회 처리) / GdprUserDeleteListener(이력 익명화)
|
||||
```
|
||||
|
||||
미들웨어는 위 Service 계층과 별도 레인에서 **매 요청마다** 동작하고, 회원탈퇴/삭제 리스너는
|
||||
코어 회원 도메인 이벤트에 반응하는 또 다른 별도 레인입니다. 세 레인 모두 같은
|
||||
`GdprConsentService`/모델을 공유하므로 그 계층 하나만 잘 유지하면 나머지는 일관됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 디렉토리
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Http/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Http/Resources/` | API 리소스 | 목록 응답은 화면이 실제로 그리는 것만 싣는다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Repositories/` | 데이터 접근 | 목록 쿼리는 컬럼 프루닝·정렬 화이트리스트 확인 |
|
||||
| `src/Models/` | Eloquent 모델 | 스키마 변경 시 마이그레이션 + 업그레이드 스텝 동반 |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/Enums/` | 상태·타입·분류 | 문자열 리터럴 대신 Enum 을 SSoT 로 둔다 |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `database/migrations/` | 마이그레이션 | 한국어 comment + `down()` 필수, 기설치본은 업그레이드 스텝으로 백필 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-gdpr --force` (빌드 불필요) |
|
||||
| `resources/routes.json` | 라우트 → 레이아웃 매핑 | `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-gdpr --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
@@ -0,0 +1,107 @@
|
||||
# GDPR (일반 데이터 보호 규정) — 데이터 모델
|
||||
|
||||
> 모델·소유 테이블·마이그레이션·Enum · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 모델
|
||||
|
||||
<!-- @generated:models START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 모델 | 테이블 | fillable | 관계 | 특성 |
|
||||
|---|---|---|---|---|
|
||||
| `GdprPolicyVersion` | `gdpr_policy_versions` | 5 | createdBy→User | - |
|
||||
| `GdprUserConsent` | `gdpr_user_consents` | 11 | user→User | - |
|
||||
| `GdprUserConsentHistory` | `gdpr_user_consent_histories` | 9 | user→User | - |
|
||||
<!-- @generated:models END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`GdprUserConsent`(mutable, "지금 동의 상태")와 `GdprUserConsentHistory`(immutable append-only,
|
||||
"동의 변경 이력")를 별도 모델·테이블로 분리한 것이 이 도메인의 핵심 결정입니다. 게이팅
|
||||
판정(`CookieConsentMiddleware`)은 상태만 읽고, 감사 대응(Art.7(1))은 이력만 봅니다. 하나로
|
||||
합쳤다면 상태를 UPDATE 할 때마다 과거 값을 별도 보존하는 로직을 매번 다시 구현해야 했을
|
||||
것입니다. `GdprPolicyVersion` 은 정책 발행 이력이며 이 역시 immutable — 발행된 버전은 그
|
||||
시점의 정책 내용을 그대로 유지해야 "그 버전에 동의했다"는 이력이 의미를 가집니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 소유 테이블
|
||||
|
||||
<!-- @generated:tables START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 테이블 | 모델 |
|
||||
|---|---|
|
||||
| `gdpr_policy_versions` | `GdprPolicyVersion` |
|
||||
| `gdpr_user_consent_histories` | `GdprUserConsentHistory` |
|
||||
| `gdpr_user_consents` | `GdprUserConsent` |
|
||||
<!-- @generated:tables END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
3개 테이블이 전부입니다 — 쿠키 카테고리 정의(`blocked_domains`/`cookie_categories`)는 별도
|
||||
테이블이 아니라 `getSettingsSchema()` 의 JSON 설정 값으로 저장됩니다(§settings.md). 별도
|
||||
테이블로 만들지 않은 이유는 그 값이 "운영자가 조정하는 설정"이지 "사용자별로 쌓이는 데이터"가
|
||||
아니기 때문입니다 — 전자는 설정 스키마, 후자만 전용 테이블을 둡니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 마이그레이션
|
||||
|
||||
<!-- @generated:migrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
마이그레이션 4개.
|
||||
|
||||
| 파일 | 생성 테이블 | 변경 테이블 | down() |
|
||||
|---|---|---|---|
|
||||
| `2026_04_27_000001_create_gdpr_user_consents_table.php` | `gdpr_user_consents` | `gdpr_user_consents` | ✅ |
|
||||
| `2026_04_27_000002_create_gdpr_user_consent_histories_table.php` | `gdpr_user_consent_histories` | `gdpr_user_consent_histories` | ✅ |
|
||||
| `2026_05_12_000003_create_gdpr_policy_versions_table.php` | `gdpr_policy_versions` | `gdpr_policy_versions` | ✅ |
|
||||
| `2026_07_14_000001_add_rejection_to_gdpr_user_consents.php` | - | `gdpr_user_consents` | ✅ |
|
||||
<!-- @generated:migrations END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`add_rejection_to_gdpr_user_consents`(2026-07-14)는 "동의 안 함"을 명시적으로 기록하기 위한
|
||||
추가입니다 — 그전에는 동의 행이 없으면 "아직 응답 안 함"과 "거부함"을 구분할 수 없었습니다.
|
||||
`ConsentAction::Rejected` 케이스와 짝을 이루는 마이그레이션입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Enum
|
||||
|
||||
<!-- @generated:enums START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| Enum | backing | case 수 | case |
|
||||
|---|---|---|---|
|
||||
| `ConsentAction` | `string` | 3 | `granted`, `revoked`, `rejected` |
|
||||
| `ConsentSource` | `string` | 6 | `banner`, `preference_center`, `register`, `mypage`, `mypage_renew_all`, `withdraw` |
|
||||
| `CookieCategory` | `string` | 4 | `cookie_necessary`, `cookie_functional`, `cookie_analytics`, `cookie_marketing` |
|
||||
| `GdprPolicyChangeType` | `string` | 3 | `material`, `non_material`, `initial` |
|
||||
<!-- @generated:enums END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`ConsentSource` 는 자기 docblock에 "어휘를 이 enum 밖(서비스/리스너 리터럴)에 흩어 두면 화면
|
||||
필터가 실제 기록 어휘의 부분집합이 되어 일부 행이 어떤 필터로도 도달하지 못한다"고 명시합니다
|
||||
(#492 과거 결함). `withdraw`(회원탈퇴 시 일괄 철회, `GdprConsentService::revokeAllOnWithdraw()`
|
||||
/ `GdprUserConsentRepository::revokeAllForUser()`)가 정확히 이 결함군으로 한 번 더 발생했던
|
||||
case입니다 — 두 지점 모두 enum이 아닌 `'withdraw'` 리터럴을 직접 기록해, 그렇게 기록된 행이
|
||||
관리자 동의 이력 화면의 어떤 출처 필터로도 걸러지지 않고 라벨도 원시 문자열로 노출됐습니다.
|
||||
`ConsentSourceVocabularyParityTest`(기록 경로가 enum 을 참조하는지 검사)가 이미 있었는데도
|
||||
놓친 이유는 두 가지입니다 — 검사 대상 파일 목록에 `GdprUserConsentRepository.php` 가
|
||||
빠져 있었고, 정규식이 `'source' =>`/`'last_source' =>` 형태만 잡아 `updateConsent(...,
|
||||
'withdraw')` 같은 **위치 인자** 형태는 못 봤습니다. `Withdraw` case 추가 + 두 지점을
|
||||
`ConsentSource::Withdraw->value` 참조로 교체 + 테스트의 스캔 대상 파일 목록에 Repository
|
||||
추가로 정정했습니다. 새 기록 지점을 추가할 때 위치 인자로 리터럴을 넘기면 이 가드가 여전히
|
||||
못 볼 수 있다는 점을 유의하세요 — 가능하면 `'source' =>`/`'last_source' =>` 형태(배열 키)를
|
||||
쓰거나 이 테스트의 정규식을 함께 넓힙니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Repository
|
||||
|
||||
<!-- @generated:repositories START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 클래스 | 종류 | 설명 |
|
||||
|---|---|---|
|
||||
| `GdprPolicyVersionRepository` | 구현 | GDPR 정책 버전 Repository 구현체 (immutable append-only) |
|
||||
| `GdprPolicyVersionRepositoryInterface` | 인터페이스 | GDPR 정책 버전 Repository 인터페이스 (immutable append-only) |
|
||||
| `GdprUserConsentHistoryRepository` | 구현 | GDPR 동의 변경 이력 Repository 구현체 (immutable append-only) |
|
||||
| `GdprUserConsentHistoryRepositoryInterface` | 인터페이스 | GDPR 동의 변경 이력 Repository 인터페이스 (immutable append-only) |
|
||||
| `GdprUserConsentRepository` | 구현 | GDPR 사용자 현재 동의 상태 Repository 구현체 |
|
||||
| `GdprUserConsentRepositoryInterface` | 인터페이스 | GDPR 사용자 현재 동의 상태 Repository 인터페이스 |
|
||||
<!-- @generated:repositories END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`GdprPolicyVersionRepository`·`GdprUserConsentHistoryRepository` 설명에 "immutable
|
||||
append-only"가 반복 명시된 것은 우연이 아닙니다 — 이 두 Repository 에는 `update()`/`delete()`
|
||||
류 메서드를 추가하지 않습니다(§AGENTS.md 금지 패턴). 상태를 고치는 메서드가 필요하다면 그것은
|
||||
`GdprUserConsentRepository`(mutable) 의 몫이며, 두 종류를 같은 Repository 에 섞으면 "이
|
||||
메서드가 이력을 고치는지 상태를 고치는지"를 매 호출부에서 다시 확인해야 합니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,128 @@
|
||||
# GDPR (일반 데이터 보호 규정) — 확장점
|
||||
|
||||
> 발행/구독 훅·미들웨어·채널·스케줄 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 발행 훅
|
||||
|
||||
<!-- @generated:hooks-published START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
발행 훅 2종 / 호출 지점 12곳. 훅 이름이 상수·변수로 조립된 호출이 12곳 있어 호출 위치가 표에 다 실리지 않을 수 있습니다.
|
||||
|
||||
| 훅 이름 | 유형 | 설명 | 발행 위치 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-gdpr.consent.granted` | action | 동의 부여 시 발화 | 선언 (호출 위치 미확인) |
|
||||
| `sirsoft-gdpr.consent.revoked` | action | 동의 철회 시 발화 | 선언 (호출 위치 미확인) |
|
||||
<!-- @generated:hooks-published END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
두 훅 모두 `GdprUserConsent $consent, string $source` 를 인자로 넘깁니다. `$source` 값은
|
||||
`banner`/`mypage`/`mypage_renew_all`/`register`/`withdraw` 중 하나이며, 어떤 화면에서 동의가
|
||||
바뀌었는지 구분해야 하는 리스너(예: 배너 동의만 특정 방식으로 처리하고 싶은 경우)는 이 값으로
|
||||
분기합니다. `revoked` 는 명시적 철회(마이페이지)뿐 아니라 회원탈퇴로 인한 일괄 철회
|
||||
(`source=withdraw`)에서도 발화됩니다 — "동의 취소" 이벤트를 하나로 통일해 구독자가 두 경로를
|
||||
따로 처리하지 않아도 되게 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 구독 훅
|
||||
|
||||
<!-- @generated:hooks-subscribed START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 훅 이름 | 유형 | 리스너 | 메서드 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| `core.auth.logout` | action (미선언) | `GdprAuthLogoutListener` | `forgetGdprCookies` | 10 |
|
||||
| `core.auth.record_consents` | action (미선언) | `GdprAuthConsentListener` | `recordRegisterConsents` | 10 |
|
||||
| `core.user.after_withdraw` | action (미선언) | `GdprUserWithdrawListener` | `handleWithdraw` | 10 |
|
||||
| `core.user.before_delete` | action (미선언) | `GdprUserDeleteListener` | `cascadePluginData` | 10 |
|
||||
<!-- @generated:hooks-subscribed END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`core.auth.record_consents` 는 회원가입 폼에서 받은 동의 값을 코어가 이 플러그인에 **위임**하는
|
||||
자리입니다 — 회원가입 컨트롤러는 동의 저장 로직을 몰라도 되고, 이 플러그인이 폼 데이터에서
|
||||
동의 관련 키만 추출해 회원가입과 같은 트랜잭션에서 기록합니다. 나머지 3개
|
||||
(`logout`/`after_withdraw`/`before_delete`)는 전부 회원 생명주기 이벤트에 반응하는 정리 로직이며,
|
||||
§핵심 흐름(AGENTS.md)에서 다룬 대로 탈퇴와 완전삭제는 반드시 구분해서 처리합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 훅 리스너
|
||||
|
||||
<!-- @generated:listeners START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 리스너 | 구독 훅 | 등록 방식 | HookListenerInterface | 파일 |
|
||||
|---|---|---|---|---|
|
||||
| `GdprAuthConsentListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/GdprAuthConsentListener.php` |
|
||||
| `GdprAuthLogoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/GdprAuthLogoutListener.php` |
|
||||
| `GdprUserDeleteListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/GdprUserDeleteListener.php` |
|
||||
| `GdprUserWithdrawListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/GdprUserWithdrawListener.php` |
|
||||
<!-- @generated:listeners END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
4개 리스너가 전부 훅 1개씩만 구독하는 것은 각자 트리거가 회원 생명주기의 서로 다른 순간
|
||||
(로그인 로그아웃/동의 기록/탈퇴/완전삭제)이라 합쳐도 이득이 없기 때문입니다.
|
||||
`GdprAuthLogoutListener` 는 로그아웃 시 `gdpr_session` 게스트 쿠키를 폐기합니다 — 로그인
|
||||
후에는 신원이 회원으로 바뀌므로, 로그아웃 시 남아 있는 게스트 세션 쿠키가 다음 방문자와
|
||||
뒤섞이지 않도록 정리하는 것입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 레이아웃 확장
|
||||
|
||||
<!-- @generated:layout-extensions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 대상 | 설명 |
|
||||
|---|---|
|
||||
| `resources/extensions/cookie_banner.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/mypage_privacy_tab.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
<!-- @generated:layout-extensions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`cookie_banner.json` 은 사이트 전역에 배너를 띄우는 조각(코어/템플릿 공용 확장 지점에 주입)이고,
|
||||
`mypage_privacy_tab.json` 은 마이페이지에 "개인정보/동의 관리" 탭을 추가하는 조각입니다. 둘 다
|
||||
`sirsoft-basic` 템플릿의 확장 지점에 의존하므로, 다른 사용자 템플릿을 쓰려면 그 템플릿에도
|
||||
같은 지점이 있어야 두 UI 가 정상 노출됩니다(README "다른 확장과의 연동" 참고).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 미들웨어
|
||||
|
||||
<!-- @generated:middleware START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 미들웨어 | 부착 대상(targets) | 우선순위 |
|
||||
|---|---|---|
|
||||
| `CookieConsentMiddleware` | `everything` | - |
|
||||
<!-- @generated:middleware END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
대상이 `everything`(모든 요청)인 이유는 functional 미동의 상태에서 어느 응답이 쿠키를
|
||||
심으려 하는지 이 플러그인이 미리 알 수 없기 때문입니다 — 특정 라우트만 골라 부착하면 그
|
||||
목록에서 빠진 응답의 쿠키는 게이팅되지 않습니다. 등록은 `GdprServiceProvider::boot()` 에서
|
||||
Laravel 커널의 `prependMiddlewareToGroup('web'|'api')` 로 이뤄지며, 코어의 미들웨어
|
||||
self-gate 규정(대상 명시)에 대한 근거가 바로 이 전면 적용 필요성입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 브로드캐스트 채널
|
||||
|
||||
<!-- @generated:channels START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 브로드캐스트 채널이 없습니다._
|
||||
<!-- @generated:channels END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
동의 상태 변경은 실시간 브로드캐스트 대상이 아닙니다 — 같은 방문자가 여러 탭을 열어둔 상태를
|
||||
동기화해야 할 만큼 시급한 이벤트가 아니고, 다음 페이지 요청 시 미들웨어가 최신 상태를 다시
|
||||
평가하므로 자연스럽게 수렴합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 스케줄
|
||||
|
||||
<!-- @generated:schedules START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 스케줄이 없습니다._
|
||||
<!-- @generated:schedules END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
동의 이력은 영구 보존이 원칙(Art.30)이라 배치로 정리할 대상이 없습니다. 게스트 세션 데이터의
|
||||
만료·정리는 코어 세션 메커니즘에 위임하며, 이 플러그인이 별도 정리 스케줄을 두지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 알림 정의
|
||||
|
||||
<!-- @generated:notifications START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 알림 정의가 없습니다._
|
||||
<!-- @generated:notifications END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
동의 부여/철회는 방문자 본인의 조작 결과이므로 본인에게 알림을 보낼 이유가 없고, 정책 버전
|
||||
발행처럼 운영자가 이미 인지하고 수행한 조작도 마찬가지입니다. 관리자에게 알려야 할 이벤트가
|
||||
생기면(예: 대량 철회 급증 같은 이상 신호) 그때 코어 알림 정의를 신설합니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,84 @@
|
||||
# GDPR (일반 데이터 보호 규정) — 프론트엔드
|
||||
|
||||
> 레이아웃·액션 핸들러·전역 진입점·에셋 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 레이아웃
|
||||
|
||||
<!-- @generated:layouts START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
레이아웃 4개 (루트: `resources/layouts`).
|
||||
|
||||
| 그룹 | 개수 |
|
||||
|---|---|
|
||||
| `admin` | 4개 |
|
||||
|
||||
| 레이아웃 | 그룹 | 종류 | extends |
|
||||
|---|---|---|---|
|
||||
| `gdpr_consent_log` | `admin` | 화면 | `_admin_base` |
|
||||
| `_policy_version_snapshot_modal` | `admin` | partial | - |
|
||||
| `_policy_version_publish_modal` | `admin` | partial | - |
|
||||
| `plugin_settings` | `admin` | 화면 | `_admin_base` |
|
||||
<!-- @generated:layouts END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
관리자 화면 4개뿐이고 **쿠키 배너·마이페이지 동의 탭은 이 표에 없습니다** — 그 둘은
|
||||
"레이아웃"이 아니라 "레이아웃 확장 조각"(`resources/extensions/cookie_banner.json`,
|
||||
`mypage_privacy_tab.json`, §extension-points.md)으로 다른 확장/템플릿 레이아웃에 주입되는
|
||||
형태라 별도 수집 축에 잡힙니다. 방문자가 실제로 보는 UI를 찾으려면 이 표가 아니라
|
||||
확장점 문서를 봐야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 액션 핸들러
|
||||
|
||||
<!-- @generated:handlers START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
핸들러 1개 (정의: `resources/js/index.ts`).
|
||||
|
||||
| 핸들러 | 레이아웃에서 부르는 이름 |
|
||||
|---|---|
|
||||
| `syncConsent` | `sirsoft-gdpr.syncConsent` |
|
||||
<!-- @generated:handlers END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`syncConsent` 하나뿐인 이유는 배너·마이페이지 UI 가 사실상 "동의 상태를 서버에 반영하고
|
||||
화면을 갱신한다"는 단일 동작만 필요로 하기 때문입니다. 자동 차단/복원 로직(외부 스크립트·
|
||||
iframe·1st-party 저장소 게이팅)은 이 액션 핸들러가 아니라 `dist/js/plugin.iife.js` 가 페이지
|
||||
로드 시 스스로 수행합니다 — 사용자 조작에 반응하는 것과 페이지 로드마다 항상 실행되는 것을
|
||||
액션 핸들러/전역 스크립트로 구분한 것입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 전역 진입점
|
||||
|
||||
<!-- @generated:frontend-entry START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 엔트리 파일 | `resources/js/index.ts` |
|
||||
| 전역 객체 | `window.__SirsoftGdpr` |
|
||||
| 재등록 진입점 | `initPlugin()` |
|
||||
|
||||
로케일 전환 시 코어가 이 진입점을 호출해 핸들러를 다시 등록합니다. 진입점은 핸들러 재등록만 수행하고 1회성 부팅 작업을 포함하지 않습니다.
|
||||
<!-- @generated:frontend-entry END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`initPlugin()` 이 "핸들러 재등록만" 하도록 좁혀 둔 것은 코어 규정(§CLAUDE.md "확장 미들웨어는
|
||||
...")을 그대로 따른 결과입니다 — 자동 차단 스크립트의 부팅(도메인 카탈로그 로드, DOM 스캔
|
||||
시작)을 여기 넣으면 로케일 전환마다 그 부팅이 중복 실행됩니다. 자동 차단 부팅은 `blocker.ts`/
|
||||
`preblocker.ts` 가 페이지 최초 로드 시 1회만 수행합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 에셋
|
||||
|
||||
<!-- @generated:assets START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 구분 |
|
||||
|---|---|
|
||||
| `dist/css/plugin.css` | 빌드 산출물 (커밋 대상) |
|
||||
| `dist/js/plugin.iife.js` | 빌드 산출물 (커밋 대상) |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 (manifest) |
|
||||
|
||||
로딩 설정: `{"strategy":"global","priority":0,"dependencies":[]}`
|
||||
<!-- @generated:assets END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`priority: 0`(다른 확장보다 먼저 로드)인 이유는 자동 차단이 **다른 확장의 추적 스크립트가
|
||||
실행되기 전에** 걸려 있어야 하기 때문입니다 — 이 플러그인이 늦게 로드되면 동의 없이 이미
|
||||
로드된 스크립트를 사후에 막을 방법이 없습니다. `strategy: "global"` 도 같은 이유로, 특정
|
||||
페이지에서만 지연 로드하면 그 페이지에서는 동의 전 차단이 통째로 빠집니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,96 @@
|
||||
# GDPR (일반 데이터 보호 규정) — 설정·권한·라우트
|
||||
|
||||
> 설정 스키마·권한·메뉴·라우트·의존 관계 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설정 스키마
|
||||
|
||||
<!-- @generated:settings-schema START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 타입 | 기본값 | 설명 |
|
||||
|---|---|---|---|
|
||||
| `privacy_policy_slug` | `string` | `privacy` | 개인정보처리방침 페이지 슬러그 |
|
||||
| `legal_entity_name` | `string` | - | 운영 주체명 |
|
||||
| `data_storage_location` | `string` | - | 데이터 저장 위치 |
|
||||
| `banner_enabled` | `boolean` | `true` | 쿠키 배너 노출 |
|
||||
| `banner_position` | `string` | `bottom_bar` | 배너 위치 |
|
||||
| `blocked_domains` | `json` | `{"functional":["*.crisp.chat","client.crisp.chat","*.intercom.io","widget.intercom.io","*.tawk.to","embed.tawk.to","cdn.weglot.com","*.weglot.com","*.usercentrics.eu"],"analytics":["google-analytics.com","*.google-analytics.com","googletagmanager.com","*.googletagmanager.com","ssl.google-analytics.com","*.hotjar.com","static.hotjar.com","*.mixpanel.com","cdn.mxpnl.com","*.amplitude.com","cdn.amplitude.com","*.segment.io","*.segment.com","wcs.naver.net","wcs.naver.com","*.beusable.net"],"marketing":["facebook.net","connect.facebook.net","facebook.com","*.facebook.com","doubleclick.net","*.doubleclick.net","googleadservices.com","googlesyndication.com","ads.google.com","*.criteo.com","static.criteo.net","*.adnxs.com","*.taboola.com","cdn.taboola.com","*.outbrain.com","*.kakao.com","analytics.ad.daum.net","platform.twitter.com","*.twitter.com","platform.linkedin.com","*.linkedin.com"]}` | 추적 도메인 차단 목록 |
|
||||
| `cookie_categories` | `json` | `[]` | 쿠키 카테고리 정의 |
|
||||
|
||||
기본값 파일: `config/settings/defaults.json` · 설정 화면 레이아웃: `resources/layouts/admin/plugin_settings.json`
|
||||
<!-- @generated:settings-schema END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`blocked_domains` 가 카테고리별(functional/analytics/marketing) 배열을 담은 단일 JSON 컬럼인
|
||||
것은, 카테고리 추가·삭제가 스키마 변경이 아니라 값 변경으로 끝나게 하기 위해서입니다 — 새
|
||||
카테고리를 추가할 때 마이그레이션이 필요하지 않습니다(다만 배너 UI 의 카테고리 목록은 별도
|
||||
동기화가 필요합니다, §AGENTS.md 수정 시 동반 의무). `cookie_categories` 가 기본값 `[]` 로
|
||||
비어 있는 것은 4대 표준 카테고리(필수/기능/분석/마케팅)가 이미 코드/Enum(`CookieCategory`)에
|
||||
고정돼 있어, 이 설정은 그 표준을 벗어나는 **추가** 카테고리를 위한 자리이기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 권한
|
||||
|
||||
<!-- @generated:permissions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 카테고리 | 이름 | 액션 | 라우트 키 |
|
||||
|---|---|---|---|
|
||||
| `privacy` | 개인정보 보호 | `view`, `update` | - |
|
||||
<!-- @generated:permissions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
권한이 `view`/`update` 하나씩만 있고 관리자 동의 이력·정책 버전 발행이 별도 권한으로 세분화
|
||||
되지 않은 것은, 이 플러그인의 관리자 기능 전체가 "개인정보 보호 담당자"라는 하나의 역할
|
||||
단위로 다뤄지기 때문입니다. 조회와 변경(설정 수정·정책 발행)을 분리해 둔 것은 감사 목적으로
|
||||
"누가 정책을 발행했는지"와 "누가 그냥 보기만 했는지"를 구분할 필요가 있어서입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 메뉴
|
||||
|
||||
<!-- @generated:menus START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 구분 | slug | 이름 | URL | 하위 |
|
||||
|---|---|---|---|---|
|
||||
| 관리자 | `sirsoft-gdpr-consent-log` | GDPR 동의 이력 | `/admin/plugins/sirsoft-gdpr/consent-log` | - |
|
||||
<!-- @generated:menus END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
"GDPR 설정"(정책 버전 발행 포함) 화면은 별도 메뉴가 아니라 플러그인 공통 설정 화면
|
||||
(`관리자 → 플러그인 → GDPR 설정`)에 얹혀 있고, "동의 이력" 조회만 독립 메뉴입니다 — 설정은
|
||||
가끔 바꾸는 화면이라 플러그인 목록에서 진입해도 충분하지만, 동의 이력은 자주 확인하는
|
||||
운영 화면이라 별도 메뉴로 빠르게 도달할 수 있어야 하기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 라우트
|
||||
|
||||
<!-- @generated:routes START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 파일 | URL prefix |
|
||||
|---|---|---|
|
||||
| `api` | `src/routes/api.php` | `/api/plugins/sirsoft-gdpr/...` |
|
||||
|
||||
확장 라우트는 **활성 상태인 확장의 것만** 등록됩니다. 라우트 정의를 바꾸면 라우트 캐시 재생성이 필요합니다.
|
||||
<!-- @generated:routes END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`Public\GdprCookieConsentController`/`GdprSettingsController` 는 인증 없이 호출됩니다 —
|
||||
게스트 방문자도 배너를 봐야 하고 동의를 기록해야 하므로, 공개 라우트로 두되 게스트 식별은
|
||||
`gdpr_session` 서명 쿠키로 처리합니다. 반대로 관리자·마이페이지 라우트는 코어 인증/권한
|
||||
미들웨어가 걸립니다 — 이 셋을 하나의 미들웨어 그룹으로 묶지 않고 컨트롤러 계층(Public/User/Admin)
|
||||
으로 분리한 것이 인증 요구사항의 차이를 드러냅니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 의존 관계
|
||||
|
||||
<!-- @generated:dependencies START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
없음 — 코어만으로 동작합니다.
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
없음.
|
||||
<!-- @generated:dependencies END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
formal `dependencies` 선언이 둘 다 "없음"인데도 README 는 `sirsoft-page`(소프트, 런타임 체크)와
|
||||
`sirsoft-basic`(레이아웃 확장 주입 지점)을 명시합니다 — 둘 다 manifest 의존성 제약으로
|
||||
선언할 만큼 강한 결합이 아니기 때문입니다. `sirsoft-page` 미설치는 기능 저하(링크 숨김)로
|
||||
그치고, `sirsoft-basic` 미사용은 다른 템플릿이 같은 주입 지점을 제공하면 해소됩니다 — 둘 다
|
||||
"없으면 설치가 막히는" 수준의 의존이 아니라 manifest 의존성 목록에 넣지 않습니다.
|
||||
<!-- @intent END -->
|
||||
@@ -201,6 +201,7 @@ return [
|
||||
'register' => 'Sign-up',
|
||||
'mypage' => 'MyPage',
|
||||
'mypage_renew_all' => 'MyPage bulk re-consent',
|
||||
'withdraw' => 'Account withdrawal',
|
||||
],
|
||||
'col' => [
|
||||
'created_at' => 'Time',
|
||||
|
||||
@@ -201,6 +201,7 @@ return [
|
||||
'register' => '회원가입',
|
||||
'mypage' => '마이페이지',
|
||||
'mypage_renew_all' => '마이페이지 일괄 재동의',
|
||||
'withdraw' => '회원탈퇴',
|
||||
],
|
||||
'col' => [
|
||||
'created_at' => '시점',
|
||||
|
||||
+2689
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plugins/sirsoft-gdpr",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"ko": "GDPR (일반 데이터 보호 규정)",
|
||||
"en": "GDPR (General Data Protection Regulation)"
|
||||
},
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"description": {
|
||||
"ko": "쿠키 동의 배너, 자동 차단, 동의 이력 저장, 마이페이지 동의 철회를 제공하는 GDPR 대응 플러그인입니다.",
|
||||
"en": "GDPR-ready plugin: cookie consent banner, auto-blocking, consent history, and mypage consent withdrawal."
|
||||
|
||||
@@ -238,7 +238,8 @@
|
||||
"preference_center": "Preferences",
|
||||
"mypage": "MyPage",
|
||||
"register": "Sign-up",
|
||||
"mypage_renew_all": "MyPage bulk re-consent"
|
||||
"mypage_renew_all": "MyPage bulk re-consent",
|
||||
"withdraw": "Account withdrawal"
|
||||
},
|
||||
"col": {
|
||||
"created_at": "Time",
|
||||
|
||||
@@ -238,7 +238,8 @@
|
||||
"preference_center": "환경설정",
|
||||
"mypage": "마이페이지",
|
||||
"register": "회원가입",
|
||||
"mypage_renew_all": "마이페이지 일괄 재동의"
|
||||
"mypage_renew_all": "마이페이지 일괄 재동의",
|
||||
"withdraw": "회원탈퇴"
|
||||
},
|
||||
"col": {
|
||||
"created_at": "시점",
|
||||
|
||||
@@ -1119,6 +1119,52 @@
|
||||
"text": "$t:sirsoft-gdpr.admin.consent_log.source.mypage_renew_all"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "inline-clickable"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "checkbox",
|
||||
"className": "checkbox",
|
||||
"checked": "{{(_local.filter.sources || []).includes('withdraw')}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "sequence",
|
||||
"params": {
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"filter.sources": "{{$event.target.checked ? [...(_local.filter.sources || []).filter(s => s !== 'withdraw'), 'withdraw'] : (_local.filter.sources || []).filter(s => s !== 'withdraw')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"actionRef": "searchConsentLogs"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-label"
|
||||
},
|
||||
"text": "$t:sirsoft-gdpr.admin.consent_log.source.withdraw"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Plugins\Sirsoft\Gdpr\Enums;
|
||||
* - register: 회원가입 시 동의 (GdprAuthConsentListener)
|
||||
* - mypage: 마이페이지 동의 관리에서 변경
|
||||
* - mypage_renew_all: 정책 개정 후 마이페이지에서 일괄 재동의 (GdprConsentService::renewAll)
|
||||
* - withdraw: 회원탈퇴 시 활성 동의 일괄 철회 (GdprConsentService::revokeAllOnWithdraw)
|
||||
*/
|
||||
enum ConsentSource: string
|
||||
{
|
||||
@@ -25,6 +26,7 @@ enum ConsentSource: string
|
||||
case Register = 'register';
|
||||
case Mypage = 'mypage';
|
||||
case MypageRenewAll = 'mypage_renew_all';
|
||||
case Withdraw = 'withdraw';
|
||||
|
||||
/**
|
||||
* 사용자 친화 라벨을 반환합니다.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Plugins\Sirsoft\Gdpr\Repositories;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Plugins\Sirsoft\Gdpr\Enums\ConsentSource;
|
||||
use Plugins\Sirsoft\Gdpr\Models\GdprUserConsent;
|
||||
use Plugins\Sirsoft\Gdpr\Repositories\Contracts\GdprUserConsentRepositoryInterface;
|
||||
|
||||
@@ -14,8 +15,8 @@ class GdprUserConsentRepository implements GdprUserConsentRepositoryInterface
|
||||
/**
|
||||
* 사용자 ID와 동의 키로 동의 상태를 조회합니다.
|
||||
*
|
||||
* @param int $userId 사용자 ID
|
||||
* @param string $consentKey 동의 항목 키
|
||||
* @param int $userId 사용자 ID
|
||||
* @param string $consentKey 동의 항목 키
|
||||
* @return GdprUserConsent|null
|
||||
*/
|
||||
public function findByUserAndKey(int $userId, string $consentKey): ?GdprUserConsent
|
||||
@@ -28,7 +29,7 @@ class GdprUserConsentRepository implements GdprUserConsentRepositoryInterface
|
||||
/**
|
||||
* 사용자 ID로 모든 동의 상태를 조회합니다.
|
||||
*
|
||||
* @param int $userId 사용자 ID
|
||||
* @param int $userId 사용자 ID
|
||||
* @return Collection<int, GdprUserConsent>
|
||||
*/
|
||||
public function getAllByUserId(int $userId): Collection
|
||||
@@ -39,7 +40,7 @@ class GdprUserConsentRepository implements GdprUserConsentRepositoryInterface
|
||||
/**
|
||||
* 사용자 ID로 활성 동의(is_consented=true)만 조회합니다.
|
||||
*
|
||||
* @param int $userId 사용자 ID
|
||||
* @param int $userId 사용자 ID
|
||||
* @return Collection<int, GdprUserConsent>
|
||||
*/
|
||||
public function getActiveByUserId(int $userId): Collection
|
||||
@@ -52,14 +53,14 @@ class GdprUserConsentRepository implements GdprUserConsentRepositoryInterface
|
||||
/**
|
||||
* 가상의 비활성 동의 상태 모델을 합성합니다 (DB 미저장).
|
||||
*
|
||||
* @param int $userId 사용자 ID
|
||||
* @param string $consentKey 동의 항목 키 (cookie_ 접두사 포함)
|
||||
* @param string|null $consentCategory 카테고리
|
||||
* @param int $userId 사용자 ID
|
||||
* @param string $consentKey 동의 항목 키 (cookie_ 접두사 포함)
|
||||
* @param string|null $consentCategory 카테고리
|
||||
* @return GdprUserConsent 합성된 비활성 모델 (DB 미저장)
|
||||
*/
|
||||
public function buildVirtualStatus(int $userId, string $consentKey, ?string $consentCategory = null): GdprUserConsent
|
||||
{
|
||||
$row = new GdprUserConsent();
|
||||
$row = new GdprUserConsent;
|
||||
$row->user_id = $userId;
|
||||
$row->consent_key = $consentKey;
|
||||
$row->consent_category = $consentCategory;
|
||||
@@ -79,9 +80,9 @@ class GdprUserConsentRepository implements GdprUserConsentRepositoryInterface
|
||||
/**
|
||||
* 동의 상태 레코드를 생성하거나 업데이트합니다.
|
||||
*
|
||||
* @param int $userId 사용자 ID
|
||||
* @param string $consentKey 동의 항목 키
|
||||
* @param array $data 업데이트 데이터
|
||||
* @param int $userId 사용자 ID
|
||||
* @param string $consentKey 동의 항목 키
|
||||
* @param array $data 업데이트 데이터
|
||||
* @return GdprUserConsent
|
||||
*/
|
||||
public function upsert(int $userId, string $consentKey, array $data): GdprUserConsent
|
||||
@@ -99,7 +100,7 @@ class GdprUserConsentRepository implements GdprUserConsentRepositoryInterface
|
||||
/**
|
||||
* 사용자의 모든 활성 동의를 일괄 철회 처리합니다 (탈퇴 시 사용).
|
||||
*
|
||||
* @param int $userId 사용자 ID
|
||||
* @param int $userId 사용자 ID
|
||||
* @return int 영향받은 행 수
|
||||
*/
|
||||
public function revokeAllForUser(int $userId): int
|
||||
@@ -109,14 +110,14 @@ class GdprUserConsentRepository implements GdprUserConsentRepositoryInterface
|
||||
->update([
|
||||
'is_consented' => false,
|
||||
'revoked_at' => now(),
|
||||
'last_source' => 'withdraw',
|
||||
'last_source' => ConsentSource::Withdraw->value,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 동의 키에 동의한 사용자 수를 반환합니다.
|
||||
*
|
||||
* @param string $consentKey 동의 항목 키
|
||||
* @param string $consentKey 동의 항목 키
|
||||
* @return int
|
||||
*/
|
||||
public function countConsentedByKey(string $consentKey): int
|
||||
@@ -129,7 +130,7 @@ class GdprUserConsentRepository implements GdprUserConsentRepositoryInterface
|
||||
/**
|
||||
* 사용자 ID로 모든 동의 상태 레코드를 삭제합니다.
|
||||
*
|
||||
* @param int $userId 사용자 ID
|
||||
* @param int $userId 사용자 ID
|
||||
* @return void
|
||||
*/
|
||||
public function deleteByUserId(int $userId): void
|
||||
|
||||
@@ -73,7 +73,7 @@ class GdprConsentService
|
||||
* @param string|null $sessionId 게스트 세션 ID (회원이면 NULL)
|
||||
* @param string $consentKey 동의 항목 키
|
||||
* @param bool $value 동의 여부
|
||||
* @param string $source 변경 경로 (허용 어휘는 ConsentSource enum — banner/preference_center/register/mypage/mypage_renew_all)
|
||||
* @param string $source 변경 경로 (허용 어휘는 {@see ConsentSource} 가 SSoT)
|
||||
* @param array|null $categories 카테고리 스냅샷 (배너 일괄 변경 시)
|
||||
* @param bool $isRejection 명시적 거부 신호 (이슈 #430). 선택형 미동의 항목을 is_rejected=true 로 저장.
|
||||
* @return void
|
||||
@@ -302,7 +302,7 @@ class GdprConsentService
|
||||
// 멱등하게 작성해야 한다.
|
||||
DB::transaction(function () use ($userId, $activeConsents) {
|
||||
foreach ($activeConsents as $consent) {
|
||||
$this->updateConsent($userId, null, $consent->consent_key, false, 'withdraw');
|
||||
$this->updateConsent($userId, null, $consent->consent_key, false, ConsentSource::Withdraw->value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* E2E: 관리자 「GDPR 동의 이력」 화면 — 출처 필터 「회원탈퇴」 체크박스 (#601 문서화 세션 중 발견)
|
||||
*
|
||||
* @scenario admin_gdpr_consent_log_source_filter_withdraw
|
||||
* @effects withdraw_checkbox_visible, withdraw_filter_updates_query_and_refetches
|
||||
*
|
||||
* 배경: `ConsentSource` enum 에 `withdraw`(회원탈퇴 시 일괄 철회) case 가 없어, 그 출처로
|
||||
* 기록된 동의 이력 행이 출처 필터 어디로도 걸러지지 않던 결함을 발견해 enum·기록 지점(Service·
|
||||
* Repository)·라벨(ko/en/ja)·이 필터 체크박스를 함께 추가했다. PHPUnit 쪽은
|
||||
* `ConsentSourceVocabularyParityTest` 가 JSON 안의 `includes('withdraw')`/라벨 바인딩
|
||||
* 존재를 정적으로 검증하지만, 그 체크박스가 실제 브라우저에서 보이고 클릭 시 목록이 다시
|
||||
* 조회되는지는 별도로 확인해야 한다.
|
||||
*
|
||||
* 검증:
|
||||
* 1. 동의 이력 화면에 "회원탈퇴" 출처 필터 체크박스가 보인다
|
||||
* 2. 체크하면 URL 쿼리에 `sources` 값으로 `withdraw` 가 반영되고 목록이 재조회된다
|
||||
* 3. 다시 해제하면 `withdraw` 가 쿼리에서 빠진다
|
||||
*/
|
||||
import { test, expect, authenticatePage } from '../../fixtures/gdpr-auth';
|
||||
|
||||
const WITHDRAW_FILTER_LABEL = '회원탈퇴';
|
||||
|
||||
async function gotoConsentLog(page: import('@playwright/test').Page): Promise<void> {
|
||||
await page.goto('/admin/plugins/sirsoft-gdpr/consent-log');
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
|
||||
await expect(page.locator('#gdpr_consent_log_datagrid__body')).toBeAttached({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
// @scenario source_filter=withdraw
|
||||
// @effects withdraw_checkbox_visible
|
||||
test('#601 - 동의 이력 출처 필터에 "회원탈퇴" 체크박스가 보인다', async ({ page, privacyManageToken }) => {
|
||||
await authenticatePage(page, privacyManageToken);
|
||||
await gotoConsentLog(page);
|
||||
|
||||
const checkbox = page.getByLabel(WITHDRAW_FILTER_LABEL);
|
||||
await expect(checkbox).toBeAttached({ timeout: 10_000 });
|
||||
await expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
// @scenario source_filter=withdraw, toggle=on_then_off
|
||||
// @effects withdraw_filter_updates_query_and_refetches
|
||||
test('#601 - "회원탈퇴" 체크 시 URL 쿼리에 반영되고, 해제하면 빠진다', async ({ page, privacyManageToken }) => {
|
||||
await authenticatePage(page, privacyManageToken);
|
||||
await gotoConsentLog(page);
|
||||
|
||||
const checkbox = page.getByLabel(WITHDRAW_FILTER_LABEL);
|
||||
await checkbox.check();
|
||||
|
||||
// searchConsentLogs 가 navigate(mergeQuery) 로 sources 배열을 쿼리에 싣는다.
|
||||
await expect(page).toHaveURL(/withdraw/, { timeout: 10_000 });
|
||||
await expect(checkbox).toBeChecked();
|
||||
|
||||
await checkbox.uncheck();
|
||||
await expect(page).not.toHaveURL(/withdraw/, { timeout: 10_000 });
|
||||
});
|
||||
@@ -3,10 +3,12 @@
|
||||
namespace Plugins\Sirsoft\Gdpr\Tests;
|
||||
|
||||
use App\Enums\PermissionType;
|
||||
use App\Extension\HookManager;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Plugins\Sirsoft\Gdpr\Repositories\Contracts\GdprPolicyVersionRepositoryInterface;
|
||||
use Plugins\Sirsoft\Gdpr\Repositories\Contracts\GdprUserConsentHistoryRepositoryInterface;
|
||||
use Plugins\Sirsoft\Gdpr\Repositories\Contracts\GdprUserConsentRepositoryInterface;
|
||||
@@ -48,6 +50,39 @@ abstract class PluginTestCase extends TestCase
|
||||
$this->app->bind(GdprUserConsentRepositoryInterface::class, GdprUserConsentRepository::class);
|
||||
$this->app->bind(GdprUserConsentHistoryRepositoryInterface::class, GdprUserConsentHistoryRepository::class);
|
||||
$this->app->bind(GdprPolicyVersionRepositoryInterface::class, GdprPolicyVersionRepository::class);
|
||||
|
||||
$this->registerPluginApiRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 API 라우트를 테스트 앱에 등록합니다.
|
||||
*
|
||||
* `PluginRouteServiceProvider` 는 `plugins` 테이블의 활성 행을 대조해서만 라우트를
|
||||
* 등록합니다(#603). 테스트는 `RefreshDatabase` 로 매번 빈 테이블에서 부팅하므로 그
|
||||
* 게이트가 항상 닫히고, 이 플러그인의 모든 엔드포인트가 404 가 됩니다 — 실패는
|
||||
* 권한·검증이 아니라 "주소 없음" 으로 나타나 원인이 드러나지 않습니다.
|
||||
*
|
||||
* 활성 행을 심는 것으로는 낫지 않습니다. 라우트 등록은 `parent::setUp()` 의 앱 부팅
|
||||
* 시점에 끝나고 DB 초기화는 그 뒤에 오기 때문입니다. 다른 번들 확장의 테스트 베이스도
|
||||
* 같은 이유로 라우트 파일을 직접 그룹에 물립니다.
|
||||
*
|
||||
* prefix·name·middleware 는 프로바이더와 동일하게 맞춥니다 — 어긋나면 테스트가
|
||||
* 통과해도 운영 주소와 다른 곳을 밟게 됩니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerPluginApiRoutes(): void
|
||||
{
|
||||
$apiRoutesFile = dirname(__DIR__).'/src/routes/api.php';
|
||||
|
||||
if (! file_exists($apiRoutesFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Route::prefix('api/plugins/sirsoft-gdpr')
|
||||
->name('api.plugins.sirsoft-gdpr.')
|
||||
->middleware('api')
|
||||
->group($apiRoutesFile);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,10 +104,10 @@ abstract class PluginTestCase extends TestCase
|
||||
*/
|
||||
private function snapshotHookManager(): void
|
||||
{
|
||||
$ref = new \ReflectionClass(\App\Extension\HookManager::class);
|
||||
$ref = new \ReflectionClass(HookManager::class);
|
||||
$this->hookSnapshot = [
|
||||
'hooks' => $ref->getProperty('hooks')->getValue(),
|
||||
'filters' => $ref->getProperty('filters')->getValue(),
|
||||
'hooks' => $ref->getProperty('hooks')->getValue(),
|
||||
'filters' => $ref->getProperty('filters')->getValue(),
|
||||
'dispatching' => $ref->getProperty('dispatching')->getValue(),
|
||||
];
|
||||
}
|
||||
@@ -88,7 +123,7 @@ abstract class PluginTestCase extends TestCase
|
||||
return;
|
||||
}
|
||||
|
||||
$ref = new \ReflectionClass(\App\Extension\HookManager::class);
|
||||
$ref = new \ReflectionClass(HookManager::class);
|
||||
$ref->getProperty('hooks')->setValue(null, $this->hookSnapshot['hooks']);
|
||||
$ref->getProperty('filters')->setValue(null, $this->hookSnapshot['filters']);
|
||||
$ref->getProperty('dispatching')->setValue(null, $this->hookSnapshot['dispatching']);
|
||||
@@ -165,17 +200,17 @@ abstract class PluginTestCase extends TestCase
|
||||
{
|
||||
$paths = ['database/migrations'];
|
||||
foreach (glob(base_path('modules/_bundled/*/database/migrations'), GLOB_ONLYDIR) as $p) {
|
||||
$paths[] = str_replace(base_path() . DIRECTORY_SEPARATOR, '', $p);
|
||||
$paths[] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $p);
|
||||
}
|
||||
foreach (glob(base_path('plugins/_bundled/*/database/migrations'), GLOB_ONLYDIR) as $p) {
|
||||
$paths[] = str_replace(base_path() . DIRECTORY_SEPARATOR, '', $p);
|
||||
$paths[] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $p);
|
||||
}
|
||||
|
||||
return [
|
||||
'--drop-views' => $this->shouldDropViews(),
|
||||
'--drop-types' => $this->shouldDropTypes(),
|
||||
'--seed' => false,
|
||||
'--path' => $paths,
|
||||
'--seed' => false,
|
||||
'--path' => $paths,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use Plugins\Sirsoft\Gdpr\Enums\ConsentSource;
|
||||
*
|
||||
* DB·라우트에 의존하지 않는 정적 검사이므로 `Tests\TestCase` 가 아니라 순수 TestCase 를 상속합니다.
|
||||
*/
|
||||
// audit:allow test-extension-base-class reason: 파일 내용·enum 값만 비교하는 순수 정적 검사 — DB/라우트/오토로드 부팅이 필요 없어 PluginTestCase 상속 시 불필요한 부팅 비용만 늘어난다
|
||||
class ConsentSourceVocabularyParityTest extends TestCase
|
||||
{
|
||||
/** 플러그인 루트 경로 */
|
||||
@@ -36,9 +37,14 @@ class ConsentSourceVocabularyParityTest extends TestCase
|
||||
$declared = ConsentSource::allValues();
|
||||
|
||||
// 실제 기록 지점 — source:/'source' =>/'last_source' => 인자로 넘어가는 리터럴
|
||||
//
|
||||
// Repository 도 포함한다 — GdprUserConsentRepository::revokeAllForUser() 가
|
||||
// Service 를 거치지 않고 직접 'last_source' => 리터럴을 UPDATE 쿼리에 싣는다.
|
||||
// 이 파일이 빠져 있으면 그 리터럴은 어떤 축으로도 검사되지 않는다.
|
||||
$sources = [
|
||||
'src/Listeners/GdprAuthConsentListener.php',
|
||||
'src/Services/GdprConsentService.php',
|
||||
'src/Repositories/GdprUserConsentRepository.php',
|
||||
'src/Http/Controllers/User/GdprConsentController.php',
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# KG 이니시스 — 에이전트 가이드
|
||||
|
||||
> 이 문서는 이 플러그인을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 [README.md](README.md) 를 보세요.
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 유형: 플러그인 (sirsoft-pay_kginicis) — KG 이니시스 PG 연동(PC/모바일/가상계좌/에스크로/일본 CBT). 소유 테이블 없음 — 상태는 sirsoft-ecommerce 소유
|
||||
2. 확장 방식: `RegisterPgProviderListener`/`RegisterCashReceiptProviderListener` 로 이커머스 레지스트리에 등록 — 이커머스 코드는 이 플러그인을 모른다
|
||||
3. 건드리면 안 되는 것: `authUrl`/`P_REQ_URL`/`netCancelUrl` 화이트리스트 검증 생략, 콜백 재처리 방지 로직 우회, IP 화이트리스트 미들웨어(`InicisNotifyIpWhitelist`) 미부착
|
||||
4. 작업 위치: `plugins/_bundled/sirsoft-pay_kginicis` — 활성 디렉토리 직접 수정 금지
|
||||
5. 반영: `php artisan plugin:update sirsoft-pay_kginicis --force`
|
||||
```
|
||||
|
||||
## 1. 이 확장은 무엇인가
|
||||
|
||||
<!-- @intent START -->
|
||||
KG 이니시스 PG(결제 게이트웨이)를 `sirsoft-ecommerce`에 연결하는 어댑터입니다. 결제수단마다
|
||||
프로토콜이 다릅니다 — PC 는 브라우저 결제창 + 서버 승인 API, 모바일은 폼 POST 이동 + 별도
|
||||
승인 API, 일본 CBT 는 완전히 다른 인증/승인 체계(JPPG)를 씁니다. 이 플러그인의 역할은 그
|
||||
세 가지 서로 다른 프로토콜을 전부 흡수해 이커머스 쪽에는 "결제 성공/실패/취소"라는 하나의
|
||||
결과만 넘기는 것입니다.
|
||||
|
||||
**설계 원칙**: 이 플러그인은 상태를 소유하지 않습니다(§data-model.md — 모델·테이블 0개).
|
||||
주문·결제 상태는 전부 `sirsoft-ecommerce`의 테이블에 있고, 이 플러그인은 PG API 와 그 상태를
|
||||
동기화하는 역할만 합니다. 등록도 코드 결합이 아니라 훅 기반입니다
|
||||
(`sirsoft-ecommerce.payment.registered_pg_providers` 필터) — 이커머스 모듈은 이 플러그인의
|
||||
존재를 컴파일 타임에 몰라도 됩니다.
|
||||
|
||||
**의도적으로 하지 않는 것**: 결제 실패 시 자동으로 다른 PG 로 재시도하지 않습니다 — PG 마다
|
||||
가맹점 계약·결제수단이 다르므로 자동 전환은 이중 결제·과금 위험을 만듭니다. 또한 일본 결제
|
||||
설정이 불완전할 때 한국 표준결제로 조용히 대체하지 않고 결제 자체를 중단합니다 — 설정 실수를
|
||||
"어쨌든 결제는 된다"로 감추면 잘못된 통화·수수료로 승인될 수 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 2. 디렉토리 지도
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Repositories/` | 데이터 접근 | 목록 쿼리는 컬럼 프루닝·정렬 화이트리스트 확인 |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-pay_kginicis --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-pay_kginicis --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-pay_kginicis --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-pay_kginicis --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-pay_kginicis --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
|
||||
## 3. 핵심 흐름
|
||||
|
||||
<!-- @intent START -->
|
||||
**PC 결제 승인**: `PaymentCallbackController`(KG 이니시스가 POST 하는 authToken/authUrl 수신)
|
||||
→ `authUrl` 화이트리스트 검증 → `sirsoft-pay_kginicis.payment.before_authorize` 훅 →
|
||||
`KgInicisApiService` 가 승인 API 호출 → `sirsoft-pay_kginicis.payment.after_authorize` 훅 →
|
||||
이커머스 주문 결제 완료 처리. 승인 후 로컬 처리 실패 시 `netCancelUrl` 로 망취소를 시도합니다
|
||||
— 이 지점이 실패하면 "PG 는 승인, 우리는 실패"인 가장 위험한 상태이므로 반드시 오류 로그를
|
||||
남깁니다.
|
||||
|
||||
**결제 취소(환불)**: 관리자가 주문 취소(`cancel_pg=true`) → 코어가
|
||||
`sirsoft-ecommerce.payment.refund` 필터 발화 → 이 플러그인의 `PaymentRefundListener`
|
||||
(우선순위 10)가 먼저 KG 이니시스 취소 API 호출 → `CancelActivityLogListener`(우선순위 20)가
|
||||
그 결과(PG 응답 시각·취소 TID)를 활동 로그에 별도 기록. 우선순위 순서가 중요합니다 — 취소가
|
||||
실제로 성공한 뒤에야 로그를 남겨야 "로그는 있는데 실제 취소는 실패"가 생기지 않습니다.
|
||||
|
||||
**일본 CBT 승인**: `/payment/cbt/hash-data` 로 해시 생성(타임스탬프 신선도 검증) →
|
||||
CBT 인증 URL 로 폼 POST → KG 이니시스가 `sid` 를 콜백으로 전달 → `cbtapprove` API 호출 →
|
||||
카드/PayPay 는 즉시 완료, 편의점은 입금대기로 저장 후 별도 NOTI 수신 시 완료. 로컬 후속
|
||||
처리가 실패하면 CBT 전용 취소 API 로 자동 취소를 시도하고, 그마저 실패하면 수동 취소가
|
||||
필요하다는 오류 로그를 남깁니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 4. 확장점
|
||||
|
||||
<!-- @generated:extension-points-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 확장점 | 수 | 상세 |
|
||||
|---|---|---|
|
||||
| 발행 훅 | 6개 | [발행 훅](docs/extension-points.md#발행-훅) |
|
||||
| 구독 훅 | 14개 | [구독 훅](docs/extension-points.md#구독-훅) |
|
||||
| 훅 리스너 | 11개 | [훅 리스너](docs/extension-points.md#훅-리스너) |
|
||||
| 레이아웃 확장 | 4개 | [레이아웃 확장](docs/extension-points.md#레이아웃-확장) |
|
||||
| 미들웨어 | 1개 | [미들웨어](docs/extension-points.md#미들웨어) |
|
||||
| 브로드캐스트 채널 | 0개 | [브로드캐스트 채널](docs/extension-points.md#브로드캐스트-채널) |
|
||||
| 스케줄 | 0개 | [스케줄](docs/extension-points.md#스케줄) |
|
||||
| 알림 정의 | 0개 | [알림 정의](docs/extension-points.md#알림-정의) |
|
||||
<!-- @generated:extension-points-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`before_authorize`/`before_cancel`/`before_cbt_refund` 는 PG 호출 **전** 개입 지점입니다 —
|
||||
예를 들어 고액 결제에 본인인증을 추가로 요구하고 싶은 확장이 `before_cancel` 을 잡아 조건
|
||||
미충족 시 예외를 던지면 KG 이니시스 API 호출 자체가 일어나지 않습니다(`before_cancel` 의
|
||||
용도로 이미 "본인인증 등 확장 지점"이라 발행 위치에 명시돼 있습니다). `after_*` 훅은 PG 응답을
|
||||
받은 뒤 부가효과(추가 로그, 알림 등)를 붙이는 자리입니다. 구독 훅 14개 중 다수가
|
||||
`core.layout_extension.after_apply` 인 이유는 관리자 주문 목록/상세 화면에 "테스트 모드
|
||||
배지"·"거래 조회 UI"를 레이아웃 확장으로 주입하기 때문입니다(§레이아웃 확장).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 5. 수정 시 동반 의무
|
||||
|
||||
- [ ] `_bundled` 에서만 수정하고 `php artisan plugin:update sirsoft-pay_kginicis --force` 로 반영
|
||||
- [ ] manifest version 상향 시 `package.json` · `package-lock.json` · `composer.json` 동기화 + CHANGELOG 기재
|
||||
- [ ] 발행 훅 추가·이름 변경 시 `php artisan ext:docgen` 재실행 (구독하는 확장의 계약이 바뀝니다)
|
||||
- [ ] API 표면 변경 시 `php artisan api:docgen --scope=plugin:sirsoft-pay_kginicis` 재실행 + `docs/api/**` 갱신
|
||||
- [ ] 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
|
||||
- [ ] TSX/TS 변경 시 `--production` 재빌드 후 `dist/` 커밋 (sourceMappingURL 잔존 금지)
|
||||
- [ ] 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
|
||||
- [ ] 승인/취소 흐름을 고칠 때 `before_*`/`after_*` 훅 순서와 우선순위(`PaymentRefundListener` < `CancelActivityLogListener`)를 유지 — 로그가 실제 처리보다 먼저 실행되면 안 된다
|
||||
- [ ] IP 화이트리스트(`InicisNotifyIpWhitelist`) 대상 라우트를 추가/변경하면 미들웨어 부착 대상(targets)도 함께 갱신
|
||||
- [ ] 새 결제수단·통화를 추가하면 그 결제수단의 콜백 URL을 관리자 설정 안내(README "콜백/통보 URL 등록")에도 반영
|
||||
|
||||
## 6. 금지 패턴
|
||||
|
||||
<!-- @intent START -->
|
||||
| 금지 | 올바른 사용 | 이유 |
|
||||
|---|---|---|
|
||||
| PG 콜백의 `authUrl`/`P_REQ_URL`/`netCancelUrl`을 화이트리스트 없이 그대로 호출 | KG 이니시스 허용 URL 목록과 대조 후에만 호출 | 콜백 파라미터를 신뢰하면 공격자가 임의 URL로 서버발 요청을 유도할 수 있다(SSRF) |
|
||||
| 동일 거래번호 콜백을 매번 재처리 | 콜백 재처리 방지 검사를 거친 뒤 처리 | 재처리를 막지 않으면 같은 결제가 중복 완료 처리되거나 중복 환불될 수 있다 |
|
||||
| 결제창 서명/모바일 해시/CBT 해시 요청에 타임스탬프 검증 생략 | 타임스탬프 신선도 검증 유지 | 오래된 서명 재사용(replay)으로 위조 결제 요청이 통과할 수 있다 |
|
||||
| 일본 결제 설정 미완료 시 한국 표준결제로 조용히 대체 | 설정 미완료면 결제 자체를 중단 | 통화·수수료·정산 구조가 다른 결제가 잘못된 흐름으로 승인될 수 있다 |
|
||||
| 라이브 키(사인키·INIAPI 키/IV·해시키)를 로그·에러 메시지에 노출 | 운영 키는 항상 마스킹하거나 로그 대상에서 제외 | 노출되면 제3자가 결제창 서명을 위조할 수 있다 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 7. 테스트 실행
|
||||
|
||||
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 개수 | 위치 |
|
||||
|---|---|---|
|
||||
| PHPUnit | 35개 | `plugins/_bundled/sirsoft-pay_kginicis/tests` |
|
||||
| Vitest | 12개 | `vitest.config.ts` |
|
||||
| Playwright | 0개 | — |
|
||||
| 시나리오 매니페스트 | 2개 | `tests/scenarios` |
|
||||
|
||||
기저 TestCase: `tests/PluginTestCase.php` — 확장 테스트는 이 클래스를 상속합니다 (`Tests\TestCase` 직접 상속 금지).
|
||||
|
||||
```bash
|
||||
# PHPUnit (변경 범위만) (Bash)
|
||||
php vendor/bin/phpunit plugins/_bundled/sirsoft-pay_kginicis/tests --filter='<대상클래스>'
|
||||
|
||||
# Vitest (확장 디렉토리에서) (PowerShell)
|
||||
cd plugins/_bundled/sirsoft-pay_kginicis && powershell -Command "npm run test:run -- <대상>"
|
||||
|
||||
```
|
||||
|
||||
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
|
||||
<!-- @generated:test-commands END -->
|
||||
|
||||
## 8. 문서 목차
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
@@ -1,267 +1,274 @@
|
||||
# KG Inicis Plugin for G7
|
||||
# KG 이니시스
|
||||
|
||||
KG 이니시스 표준결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다.
|
||||
**G7 플러그인 · sirsoft-pay_kginicis**
|
||||
KG 이니시스 표준결제를 sirsoft-ecommerce 에 연결하는 결제 플러그인
|
||||
|
||||
PC 결제는 KG 이니시스 `INIStdPay.js` 표준결제창을 사용하고, 모바일 결제는 KG 이니시스 모바일 표준결제창으로 이동한 뒤 서버 승인 API로 최종 승인합니다. 일본 엔(JPY) 결제는 KG 이니시스 CBT(JPPG) 흐름을 사용합니다.
|
||||
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-1.1.2-0066FF?style=flat-square" alt="version 1.1.2">
|
||||
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
|
||||
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 >=7.0.10">
|
||||
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
|
||||
<img src="https://img.shields.io/badge/requires-sirsoft--ecommerce-BF8700?style=flat-square" alt="requires sirsoft-ecommerce">
|
||||
</p>
|
||||
<!-- @generated:badges END -->
|
||||
|
||||
---
|
||||
|
||||
[소개](#소개) · [주요 기능](#주요-기능) · [동작 방식](#동작-방식) · [요구 사항](#요구-사항) · [설치](#설치) · [관리자 설정](#관리자-설정) · [사용 방법](#사용-방법) · [다른 확장과의 연동](#다른-확장과의-연동) · [문서](#문서) · [트러블슈팅](#트러블슈팅) · [변경 이력](#변경-이력) · [라이선스](#라이선스)
|
||||
|
||||
---
|
||||
|
||||
## 소개
|
||||
|
||||
<!-- @intent START -->
|
||||
KG 이니시스 표준결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다. PC 결제는
|
||||
`INIStdPay.js` 표준결제창을, 모바일 결제는 모바일 표준결제창으로 이동한 뒤 서버 승인 API로
|
||||
최종 승인하는 흐름을 씁니다. 일본 엔(JPY) 결제는 별도의 KG 이니시스 CBT(JPPG) 흐름을 씁니다.
|
||||
|
||||
이 플러그인은 결제 자체의 상태(주문·결제 성공/실패/취소)를 소유하지 않습니다 — 그 상태는
|
||||
`sirsoft-ecommerce`의 주문·결제 테이블에 있고, 이 플러그인은 "그 상태를 KG 이니시스 API 와
|
||||
어떻게 주고받는가"만 책임집니다. 그래서 이 플러그인은 소유 테이블/모델이 하나도 없습니다
|
||||
(§data-model.md).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 주요 기능
|
||||
|
||||
- 신용카드, 계좌이체, 가상계좌, 휴대폰결제 지원
|
||||
- PC 표준결제창 연동
|
||||
- 모바일 표준결제창 연동 및 `P_CHKFAKE` 위변조 방지 해시 생성
|
||||
- 삼성페이, L.pay, 카카오페이 간편결제 버튼 주입
|
||||
- 가상계좌 발급, PC/모바일 입금통보 처리
|
||||
- 에스크로 결제, 배송 등록, 구매결정, 구매거절확인 연동
|
||||
- 결제 취소 및 부분취소 연동
|
||||
- PG 측 결제 취소 확인 시점의 활동 로그 별도 기록 (PG 응답 시각·취소 TID 사후 추적)
|
||||
- 주문 완료/마이페이지 영수증 버튼 주입
|
||||
- 관리자 주문 상세의 거래 조회, 에스크로 처리 UI 확장
|
||||
- 현금영수증 발급/취소 (이커머스 모듈의 공용 현금영수증 프로바이더로 등록 — PG 결제사와 독립 선택 가능)
|
||||
- 일본 결제 CBT(JPPG) 인증/승인, 테스트 상품 생성, 연결 진단
|
||||
- 승인 URL 화이트리스트, 콜백 재처리 방지, 타임스탬프 신선도 검증
|
||||
<!-- @intent START -->
|
||||
| 영역 | 설명 |
|
||||
|---|---|
|
||||
| 결제수단 | 신용카드, 계좌이체, 가상계좌, 휴대폰결제 |
|
||||
| 간편결제 | 삼성페이, L.pay, 카카오페이 버튼 주입 (다른 PG가 기본이어도 노출 가능) |
|
||||
| 가상계좌 | 발급 + PC/모바일 입금통보 처리 |
|
||||
| 에스크로 | 결제, 배송 등록, 구매결정, 구매거절확인 연동 |
|
||||
| 결제 취소 | 전액/부분취소, PG 취소 확인 시점 별도 활동 로그(PG 응답 시각·취소 TID) |
|
||||
| 영수증 | 주문 완료/마이페이지 영수증 버튼, 현금영수증 발급/취소(이커머스 공용 프로바이더) |
|
||||
| 관리자 확장 | 주문 상세 거래 조회, 에스크로 처리 UI |
|
||||
| 일본 결제(CBT) | 인증/승인, 테스트 상품 생성, 연결 진단 |
|
||||
| 보안 | 승인 URL 화이트리스트, 콜백 재처리 방지, 타임스탬프 신선도 검증 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 동작 방식
|
||||
|
||||
<!-- @intent START -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[체크아웃 주문 생성] -->|PC| B["/payment/signature 호출 → INIStdPay.js 결제창"]
|
||||
A -->|모바일| C["/payment/mobile/signature → 모바일 표준결제창"]
|
||||
A -->|JPY| D["/payment/cbt/hash-data → CBT 인증 URL"]
|
||||
B --> E["/payment/callback (authToken·authUrl)"]
|
||||
C --> F["/payment/mobile/callback (P_TID·P_REQ_URL)"]
|
||||
D --> G["/payment/cbt/callback (sid)"]
|
||||
E --> H[서버가 authUrl 화이트리스트 검증 후 승인 API 호출]
|
||||
F --> H
|
||||
G --> I[cbtapprove API 호출]
|
||||
H --> J[주문 결제 완료 처리]
|
||||
I --> J
|
||||
J --> K[성공 URL 리다이렉트]
|
||||
```
|
||||
|
||||
승인 후 로컬 처리가 실패하면 PC/모바일은 각각 netCancel/취소 API로, CBT는 CBT 전용 취소
|
||||
API로 자동 취소를 시도합니다(자동 취소까지 실패하면 수동 취소가 필요하다는 오류 로그를
|
||||
남깁니다) — "PG 는 승인됐는데 우리 시스템은 실패"라는 상태가 남지 않도록 하기 위함입니다.
|
||||
|
||||
가상계좌는 결제창에서 발급되면 주문이 입금대기 상태로 유지되다가, KG 이니시스가 입금통보
|
||||
URL로 결과를 POST 하면 거래번호 재처리·금액 검증 후 결제 완료 처리됩니다. PC/모바일 입금통보는
|
||||
URL이 다르지만 같은 IP 화이트리스트 미들웨어를 거칩니다.
|
||||
|
||||
일본 CBT 는 카드/PayPay는 즉시 승인 후 완료 처리되고, 편의점(CVS) 결제는 입금대기로 저장된 뒤
|
||||
`/payment/cbt/cvs-notify` 수신 시 완료 처리됩니다. JPY 주문은 일본 결제 설정이 완료된 경우에만
|
||||
CBT 결제창으로 진입하며, 설정이 부족하면 한국 표준결제로 대체하지 않고 결제 자체를 중단합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 요구 사항
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| G7 | `>= 7.0.0-beta.2` |
|
||||
| 의존 모듈 | `sirsoft-ecommerce >= 1.0.0-beta.4` |
|
||||
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| G7 코어 | `>=7.0.10` |
|
||||
| PHP | `^8.2` |
|
||||
| 의존 모듈 | `sirsoft-ecommerce` `>=1.1.0` |
|
||||
<!-- @generated:requirements END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
| 항목 | 필요한 것 |
|
||||
|---|---|
|
||||
| 운영 환경 | HTTPS 도메인, 올바른 `APP_URL`, KG 이니시스 가맹점 계약 정보 |
|
||||
| PC 결제 | MID, signKey |
|
||||
| 모바일 결제 | MID, 모바일 hash key |
|
||||
| 취소/거래조회/현금영수증 | INIAPI key, INIAPI IV |
|
||||
| 일본 CBT | 별도 일본 결제 MID, CBT hash key |
|
||||
|
||||
서버에서 KG 이니시스 결제/INIAPI/CBT 호스트로 HTTPS outbound 요청이 가능해야 합니다. CBT 테스트 환경 `devcbt.inicis.com`은 KG 이니시스 측에 서버 egress IP 등록이 필요할 수 있습니다.
|
||||
서버에서 KG 이니시스 결제/INIAPI/CBT 호스트로 HTTPS outbound 요청이 가능해야 합니다. CBT
|
||||
테스트 환경 `devcbt.inicis.com`은 KG 이니시스 측에 서버 egress IP 등록이 필요할 수 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 설치
|
||||
|
||||
플러그인을 G7 프로젝트의 플러그인 디렉토리에 배치합니다.
|
||||
|
||||
```text
|
||||
plugins/sirsoft-pay_kginicis
|
||||
```
|
||||
|
||||
프론트엔드 에셋을 수정한 경우 플러그인 디렉토리에서 빌드합니다.
|
||||
|
||||
<!-- @generated:install START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
# 번들 설치 (코어에 동봉된 소스에서 설치)
|
||||
php artisan plugin:install sirsoft-pay_kginicis
|
||||
|
||||
# 활성화
|
||||
php artisan plugin:activate sirsoft-pay_kginicis
|
||||
|
||||
# 업데이트 (번들 소스 기준 강제 반영)
|
||||
php artisan plugin:update sirsoft-pay_kginicis --force
|
||||
```
|
||||
|
||||
그다음 G7 관리자에서 플러그인을 활성화하고, 이커머스 결제 설정에서 PG 제공자를 `KG 이니시스`로 선택합니다.
|
||||
저장소: https://github.com/gnuboard/g7-plugin-sirsoft-pay_kginicis
|
||||
<!-- @generated:install END -->
|
||||
|
||||
설치·활성화 후 이커머스 결제 설정에서 PG 제공자를 "KG 이니시스"로 선택해야 실제로 결제
|
||||
흐름에 연결됩니다 — 활성화만으로는 체크아웃 화면에 나타나지 않습니다.
|
||||
|
||||
## 관리자 설정
|
||||
|
||||
관리자 플러그인 설정 화면에서 KG 이니시스 계약 정보를 입력합니다.
|
||||
<!-- @generated:settings-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 의미 | 기본값 |
|
||||
|---|---|---|
|
||||
| `is_test_mode` | 테스트 모드 | `true` |
|
||||
| `test_mid` | 테스트 가맹점 ID (MID) | `INIpayTest` |
|
||||
| `test_sign_key` | 테스트 사인키 | `SU5JTElURV9UUklQTEVERVNfS0VZU1RS` |
|
||||
| `test_iniapi_key` | 테스트 INIAPI 키 | `ItEQKi3rY7uvDS8l` |
|
||||
| `test_iniapi_iv` | 테스트 INIAPI IV | `HYb3yQ4f65QL89==` |
|
||||
| `live_mid` | 라이브 가맹점 ID (MID) | - |
|
||||
| `live_sign_key` | 라이브 사인키 | - |
|
||||
| `live_iniapi_key` | 라이브 INIAPI 키 | - |
|
||||
| `live_iniapi_iv` | 라이브 INIAPI IV | - |
|
||||
| `test_mobile_hash_key` | 테스트 모바일 해시키 | `3CB8183A4BE283555ACC8363C0360223` |
|
||||
| `live_mobile_hash_key` | 라이브 모바일 해시키 | - |
|
||||
| `use_escrow` | 에스크로 결제 활성화 | `false` |
|
||||
| `japan_enabled` | 일본 결제 활성화 | `false` |
|
||||
| `japan_restrict_jpy_payment_methods` | JPY 주문 결제수단 제한 | `false` |
|
||||
| `test_japan_sign_key` | 테스트 일본 CBT 해시키 | `5AL5Djb1Ipualn0F` |
|
||||
| `live_japan_mid` | 라이브 일본 MID | - |
|
||||
| `live_japan_sign_key` | 라이브 일본 CBT 해시키 | - |
|
||||
| `japan_merchant_name` | 일본 결제 가맹점명 | `サンプルストア` |
|
||||
| `japan_merchant_name_kana` | 일본 결제 가맹점명 Kana | `サンプルストア` |
|
||||
| `japan_merchant_name_alphabet` | 일본 결제 가맹점명 영문 | `Sample Store` |
|
||||
| `japan_merchant_name_short` | 일본 결제 가맹점 약칭 | `サンプル` |
|
||||
| `japan_contact_name` | 일본 결제 문의처명 | `サポート窓口` |
|
||||
| `japan_contact_email` | 일본 결제 문의 이메일 | `support@example.com` |
|
||||
| `japan_contact_phone` | 일본 결제 문의 전화번호 | `0120-123-456` |
|
||||
| `japan_contact_opening_hours` | 일본 결제 문의 영업시간 | `10:00-18:00` |
|
||||
| `redirect_success_url` | 결제 성공 리다이렉트 URL | `{shopBase}/orders/{orderId}/complete` |
|
||||
| `redirect_fail_url` | 결제 실패 리다이렉트 URL | `{shopBase}/checkout` |
|
||||
| `easy_pay_allow_with_other_pg` | 타 PG와 사용가능함 | `false` |
|
||||
| `easy_pay_samsung_pay` | KG이니시스 삼성페이 사용 | `false` |
|
||||
| `easy_pay_naverpay` | KG이니시스 네이버페이 사용 | `false` |
|
||||
| `easy_pay_show_brand_button` | 간편결제 브랜드 버튼 표시 | `false` |
|
||||
| `easy_pay_lpay` | KG이니시스 L.pay 사용 | `false` |
|
||||
| `easy_pay_kakaopay` | KG이니시스 카카오페이 사용 | `false` |
|
||||
| `use_credit_point` | 신용카드 포인트 사용 | `false` |
|
||||
|
||||
| 설정 | 설명 |
|
||||
|------|------|
|
||||
| 테스트 모드 | 활성화 시 KG 이니시스 테스트 환경을 사용합니다. 실제 카드 승인/출금 알림이 발생할 수 있으며, 테스트 거래는 매일 23:00~23:50 사이 자동 취소될 수 있습니다. |
|
||||
| 테스트 MID | 기본값은 `INIpayTest`입니다. 에스크로 테스트 사용 시 내부적으로 `iniescrow0`을 사용합니다. |
|
||||
| 테스트 사인키 | PC 결제창 서명 생성에 사용합니다. |
|
||||
| 테스트 INIAPI 키/IV | 취소, 거래조회, 현금영수증, 에스크로 API 인증에 사용합니다. |
|
||||
| 테스트 모바일 해시키 | 모바일 `P_CHKFAKE` 생성에 사용합니다. |
|
||||
| 라이브 MID | 운영 MID입니다. `SIR` prefix 없이 입력해도 플러그인이 자동 보정합니다. |
|
||||
| 라이브 사인키 | 운영 결제창 서명 생성에 사용합니다. 외부에 노출하지 마세요. |
|
||||
| 라이브 INIAPI 키/IV | 운영 취소, 거래조회, 현금영수증, 에스크로 API 인증에 사용합니다. |
|
||||
| 라이브 모바일 해시키 | 운영 모바일 `P_CHKFAKE` 생성에 사용합니다. |
|
||||
| 에스크로 결제 활성화 | PC는 `acceptmethod`에 `useescrow`, 모바일은 `P_RESERVED`에 `useescrow=Y`를 추가합니다. |
|
||||
| 일본 결제 활성화 | JPY 주문에서 KG 이니시스 CBT(JPPG) 결제 흐름을 사용합니다. |
|
||||
| 테스트 일본 CBT 해시키 | CBT 테스트 해시 생성에 사용합니다. 테스트 MID는 `CBTTEST001` 고정값을 사용합니다. |
|
||||
| 라이브 일본 MID/해시키 | 운영 CBT 결제에 사용합니다. |
|
||||
| JPPG 결제창 표시 정보 | 일본 결제창 `extraData`에 포함되는 가맹점명, 가나명, 영문명, 문의처 정보를 설정합니다. 운영 전 실제 계약 정보로 교체하세요. |
|
||||
| 결제 성공 URL | 기본값은 `/shop/orders/{orderId}/complete`입니다. |
|
||||
| 결제 실패 URL | 기본값은 `/shop/checkout`입니다. |
|
||||
| 간편결제 | KG 이니시스 계약이 완료된 간편결제만 활성화하세요. |
|
||||
| 타 PG와 사용가능함 | 다른 PG가 기본값이어도 KG 이니시스 간편결제 버튼을 체크아웃 화면에 표시합니다. |
|
||||
| 신용카드 포인트 사용 | PC 카드 결제 `acceptmethod`에 신용카드 포인트 사용 옵션을 추가합니다. |
|
||||
개발자용 상세(타입·검증·저장 위치)는 [설정 스키마](docs/settings.md#설정-스키마) 를 보세요.
|
||||
<!-- @generated:settings-summary END -->
|
||||
|
||||
테스트 모드 주문은 실제 배송하지 마세요. 실제 카드 승인/출금 알림이 발생할 수 있으며, 테스트 거래는 매일 23:00~23:50 사이 자동 취소될 수 있습니다.
|
||||
<!-- @intent START -->
|
||||
테스트 모드에서는 실제 카드 승인/출금 알림이 발생할 수 있으며, 테스트 거래는 매일
|
||||
23:00~23:50 사이 자동 취소될 수 있습니다 — **테스트 모드 주문을 실제로 배송하지 마세요.**
|
||||
운영 키(라이브 사인키·INIAPI 키/IV·모바일/CBT 해시키)는 외부에 노출하지 말고, 배포 전
|
||||
테스트 모드가 의도한 값인지 반드시 확인하세요.
|
||||
|
||||
운영 키와 해시키는 외부에 노출하지 말고, 배포 전 테스트 모드가 의도한 값인지 확인하세요.
|
||||
라이브 MID는 `SIR` 접두사 없이 입력해도 플러그인이 자동 보정합니다. 에스크로는 PC의
|
||||
`acceptmethod`에 `useescrow`를, 모바일은 `P_RESERVED`에 `useescrow=Y`를 추가하는 방식으로
|
||||
켜집니다. 일본 결제를 운영 모드로 켜려면 라이브 일본 MID/CBT 해시키와 실제 JPPG 가맹점
|
||||
표시 정보가 필요합니다 — 기본 샘플값이 남아 있으면 설정 저장 단계에서 차단됩니다.
|
||||
|
||||
## 콜백 및 통보 URL
|
||||
|
||||
KG 이니시스 가맹점 관리자에 아래 URL을 등록합니다. 도메인은 실제 운영 도메인으로 바꿔 입력하세요.
|
||||
**콜백/통보 URL 등록** — KG 이니시스 가맹점 관리자에 아래 URL을 실제 운영 도메인으로 등록합니다.
|
||||
|
||||
| 용도 | URL |
|
||||
|------|-----|
|
||||
| PC 결제 결과 Return URL | `https://your-domain.com/plugins/sirsoft-pay_kginicis/payment/callback` |
|
||||
| PC 가상계좌 입금통보 URL | `https://your-domain.com/plugins/sirsoft-pay_kginicis/payment/vbank-notify` |
|
||||
| 모바일 결제 결과 `P_NEXT_URL` | `https://your-domain.com/plugins/sirsoft-pay_kginicis/payment/mobile/callback` |
|
||||
| 모바일 가상계좌 입금통보 URL | `https://your-domain.com/plugins/sirsoft-pay_kginicis/payment/mobile/vbank-notify` |
|
||||
| CBT 콜백 URL | `https://your-domain.com/plugins/sirsoft-pay_kginicis/payment/cbt/callback` |
|
||||
| CBT 편의점 입금 NOTI URL | `https://your-domain.com/plugins/sirsoft-pay_kginicis/payment/cbt/cvs-notify` |
|
||||
| 에스크로 구매결정 화면 | `https://your-domain.com/plugins/sirsoft-pay_kginicis/payment/escrow-confirm/{orderNumber}` |
|
||||
|---|---|
|
||||
| PC 결제 결과 Return URL | `https://{도메인}/plugins/sirsoft-pay_kginicis/payment/callback` |
|
||||
| PC 가상계좌 입금통보 URL | `https://{도메인}/plugins/sirsoft-pay_kginicis/payment/vbank-notify` |
|
||||
| 모바일 결제 결과 `P_NEXT_URL` | `https://{도메인}/plugins/sirsoft-pay_kginicis/payment/mobile/callback` |
|
||||
| 모바일 가상계좌 입금통보 URL | `https://{도메인}/plugins/sirsoft-pay_kginicis/payment/mobile/vbank-notify` |
|
||||
| CBT 콜백 URL | `https://{도메인}/plugins/sirsoft-pay_kginicis/payment/cbt/callback` |
|
||||
| CBT 편의점 입금 NOTI URL | `https://{도메인}/plugins/sirsoft-pay_kginicis/payment/cbt/cvs-notify` |
|
||||
| 에스크로 구매결정 화면 | `https://{도메인}/plugins/sirsoft-pay_kginicis/payment/escrow-confirm/{orderNumber}` |
|
||||
|
||||
PC 결제 결과 Return URL과 모바일 `P_NEXT_URL`은 사용자 브라우저를 통해 호출됩니다. 가상계좌 입금통보 URL은 KG 이니시스 서버가 직접 호출하므로 운영 환경에서 IP 화이트리스트가 적용됩니다.
|
||||
가상계좌 입금통보 URL은 KG 이니시스 서버가 직접 호출하므로 운영 환경에서 IP 화이트리스트가
|
||||
적용됩니다(`203.238.37.15`, `39.115.212.9`, `118.129.210.25`, `183.109.71.153` — 운영 전
|
||||
KG 이니시스 최신 연동 가이드로 다시 확인하세요). `local`/`testing` 환경에서는 개발·테스트를
|
||||
위해 이 제한을 우회합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
KG 이니시스 PC 에스크로 매뉴얼 기준 별도 webhook 통보 채널은 사용하지 않습니다. 에스크로 배송등록, 구매결정, 구매거절확인은 플러그인이 제공하는 화면과 API를 통해 처리합니다.
|
||||
## 사용 방법
|
||||
|
||||
## IP 화이트리스트
|
||||
<!-- @intent START -->
|
||||
**결제 취소/부분취소**: 관리자가 주문 취소를 요청(`cancel_pg=true`)하면 코어가
|
||||
`sirsoft-ecommerce.payment.refund` 필터 훅을 발화하고, 이 플러그인의 `PaymentRefundListener`
|
||||
가 KG 이니시스 취소/부분취소 API를 호출합니다(전액취소는 `cancelPrice=null` +
|
||||
`totalAmount=null`, 부분취소는 취소 금액 + 원래 결제금액). 배송비가 포함된 주문은 전체취소 시
|
||||
배송비도 함께 환불 레코드에 반영되고, 쿠폰이 적용된 주문은 실결제금액(쿠폰 차감 후)이 PG
|
||||
취소 금액으로 전달됩니다. 부분취소로 쿠폰 최소 주문금액 조건을 더 이상 충족하지 못하면
|
||||
코어가 취소 자체를 거부(422)해 PG 호출이 아예 발생하지 않습니다. KG 이니시스 API 호출이
|
||||
실패하면 주문 상태 변경이 롤백됩니다.
|
||||
|
||||
운영 환경에서는 아래 IP에서 들어온 KG 이니시스 가상계좌 입금통보만 허용합니다. `local`, `testing` 환경에서는 개발과 테스트를 위해 제한을 우회합니다.
|
||||
**에스크로 처리**: 에스크로 결제 완료 후 관리자 주문 상세에서 배송 등록을 호출할 수 있고,
|
||||
사용자는 에스크로 구매결정 화면에서 구매확인을 진행합니다. 구매거절이 발생한 주문은 관리자
|
||||
주문 상세에서 구매거절확인을 호출할 수 있습니다.
|
||||
|
||||
| IP |
|
||||
|----|
|
||||
| `203.238.37.15` |
|
||||
| `39.115.212.9` |
|
||||
| `118.129.210.25` |
|
||||
| `183.109.71.153` |
|
||||
**CBT 연결 진단**: 일본 결제 테스트가 실패하면 관리자 CBT 연결 진단(§API)에서 서버 egress
|
||||
IP와 `devcbt.inicis.com` 443 연결 상태를 먼저 확인합니다.
|
||||
|
||||
운영 전 KG 이니시스 가맹점 관리자와 최신 연동 가이드의 통보 서버 IP를 다시 확인하세요.
|
||||
전체 API 목록(사용자/관리자)은 [docs/api/](docs/api/README.md) 를, 발행/구독 훅 목록은
|
||||
[docs/extension-points.md](docs/extension-points.md) 를 참고하세요.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 결제 흐름
|
||||
## 다른 확장과의 연동
|
||||
|
||||
### PC 결제
|
||||
<!-- @generated:integrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
```text
|
||||
체크아웃 주문 생성
|
||||
→ 프론트엔드 핸들러가 /payment/signature 호출
|
||||
→ INIStdPay.js 결제창 실행
|
||||
→ KG 이니시스가 /payment/callback 으로 authToken, authUrl POST
|
||||
→ 서버가 authUrl 화이트리스트 검증 후 승인 API 호출
|
||||
→ 주문 결제 완료 처리
|
||||
→ 성공 URL로 리다이렉트
|
||||
```
|
||||
| 확장 | 유형 | 버전 제약 | 번들 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-ecommerce` | 모듈 | `>=1.1.0` | ✅ |
|
||||
|
||||
승인 후 주문 처리에 실패하면 KG 이니시스 netCancel URL로 망취소를 시도합니다.
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
### 모바일 결제
|
||||
없음.
|
||||
<!-- @generated:integrations END -->
|
||||
|
||||
```text
|
||||
체크아웃 주문 생성
|
||||
→ /payment/mobile/signature 호출
|
||||
→ 모바일 표준결제창으로 form POST
|
||||
→ KG 이니시스가 /payment/mobile/callback 으로 P_TID, P_REQ_URL 전달
|
||||
→ 서버가 P_REQ_URL 화이트리스트 검증 후 모바일 승인 API 호출
|
||||
→ 주문 결제 완료 처리
|
||||
→ 성공 URL로 리다이렉트
|
||||
```
|
||||
<!-- @intent START -->
|
||||
`RegisterPgProviderListener`가 이 플러그인을 이커머스의 PG 제공자 레지스트리에, `RegisterCashReceiptProviderListener`
|
||||
가 현금영수증 프로바이더 레지스트리에 각각 등록합니다 — PG 결제사 선택과 현금영수증 발급사
|
||||
선택은 서로 독립적이라, 다른 PG를 쓰면서도 KG 이니시스로 현금영수증만 발급하는 조합이
|
||||
가능합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
모바일 승인 후 주문 처리에 실패하면 취소 API로 자동 취소를 시도합니다.
|
||||
## 문서
|
||||
|
||||
### 가상계좌
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
|
||||
```text
|
||||
결제창에서 가상계좌 발급
|
||||
→ 주문 결제 정보에 은행, 계좌번호, 예금주, 만료일 저장
|
||||
→ 주문은 입금대기 상태 유지
|
||||
→ KG 이니시스가 입금통보 URL로 입금 결과 POST
|
||||
→ 거래번호 재처리와 금액 검증 후 주문 결제 완료 처리
|
||||
```
|
||||
## 트러블슈팅
|
||||
|
||||
PC와 모바일 입금통보는 서로 다른 URL을 사용하지만, 모두 같은 IP 화이트리스트 미들웨어를 통과해야 합니다.
|
||||
<!-- @intent START -->
|
||||
| 증상 | 원인 | 조치 |
|
||||
|---|---|---|
|
||||
| 가상계좌 입금통보가 반영되지 않음 | 운영 환경 IP 화이트리스트에 KG 이니시스 통보 서버 IP가 없음 | 최신 연동 가이드의 통보 서버 IP로 화이트리스트를 갱신 |
|
||||
| 결제 승인 후 주문이 실패 상태로 남음 | 로컬 후속 처리 실패 후 자동 취소(망취소/취소 API)까지 실패 | 오류 로그의 안내대로 수동 취소 진행 — PG 승인은 이미 됐을 수 있음 |
|
||||
| 일본 결제창이 안 열리고 결제가 중단됨 | 일본 결제 설정(라이브 MID/해시키/가맹점 정보) 미완료 | 설정을 완료하거나, 완료 전까지는 JPY 주문을 받지 않음 — 한국 표준결제로 자동 대체되지 않음 |
|
||||
| CBT 테스트가 계속 실패함 | 서버 egress IP 미등록 또는 방화벽으로 443 포트 차단 | 관리자 CBT 연결 진단 실행 후 KG 이니시스에 서버 IP 등록 요청 |
|
||||
| 결제 성공했는데 간편결제 버튼 클릭 시 오류 | KG 이니시스 계약이 없는 결제수단/간편결제를 활성화 | 계약이 완료된 결제수단만 관리자 설정에서 활성화 |
|
||||
<!-- @intent END -->
|
||||
|
||||
### 일본 CBT
|
||||
## 변경 이력
|
||||
|
||||
```text
|
||||
JPY 주문 생성
|
||||
→ /payment/cbt/hash-data 호출
|
||||
→ CBT 인증 URL로 form POST
|
||||
→ KG 이니시스가 /payment/cbt/callback 으로 sid 전달
|
||||
→ 서버가 cbtapprove API 호출
|
||||
→ 카드/PayPay는 주문 결제 완료 처리
|
||||
→ 편의점(CVS)은 입금대기 저장 후 /payment/cbt/cvs-notify 입금 NOTI 수신 시 결제 완료 처리
|
||||
```
|
||||
|
||||
CBT 승인 이후 로컬 후속 처리에 실패하면 CBT 전용 취소 API로 자동 취소를 시도합니다. 자동 취소까지 실패한 경우에는 운영자 수동 취소가 필요하다는 오류 로그를 남깁니다.
|
||||
|
||||
일본 엔(JPY) 주문은 일본 결제 설정이 완료된 경우에만 CBT 결제창으로 진입합니다. 설정이 부족하면 한국 표준결제 흐름으로 대체하지 않고 결제를 중단합니다.
|
||||
|
||||
현재 CBT 결제창은 선택한 결제수단에 맞춰 지불수단을 제한합니다. 신용카드는 `CARD`만, PayPay는 `PAYpay`만, 일본 편의점결제는 `CVS`만 열립니다.
|
||||
|
||||
운영 모드에서 일본 결제를 활성화하려면 라이브 일본 MID/CBT 해시키와 실제 JPPG 가맹점 표시 정보가 필요합니다. 기본 샘플값이 남아 있으면 설정 저장 단계에서 차단됩니다.
|
||||
|
||||
### 에스크로
|
||||
|
||||
에스크로를 활성화하면 결제 요청에 에스크로 옵션을 전달합니다. 에스크로 결제 완료 후 관리자 주문 상세에서 배송 등록을 호출할 수 있고, 사용자는 에스크로 구매결정 화면에서 구매확인을 진행할 수 있습니다.
|
||||
|
||||
구매거절이 발생한 주문은 관리자 주문 상세에서 구매거절확인을 호출할 수 있습니다.
|
||||
|
||||
### 결제 취소 / 부분취소
|
||||
|
||||
```text
|
||||
관리자 주문 취소 요청 (cancel_pg=true)
|
||||
→ 코어가 sirsoft-ecommerce.payment.refund 필터 훅 발화
|
||||
→ PaymentRefundListener 가 KG 이니시스 취소/부분취소 API 호출
|
||||
· 전액취소: cancelPrice=null, totalAmount=null
|
||||
· 부분취소: cancelPrice=취소 금액, totalAmount=원래 결제금액
|
||||
→ 코어가 환불 레코드 생성 + 쿠폰 / 마일리지 / 재고 복원
|
||||
→ CancelActivityLogListener 가 PG 응답 시각·취소 TID를 활동 로그에 기록
|
||||
```
|
||||
|
||||
배송비가 포함된 주문은 전체취소 시 배송비도 함께 환불 레코드에 반영되고, 쿠폰이 적용된 주문은 실결제금액(쿠폰 차감 후) 이 PG 취소 금액으로 전달됩니다. 부분취소 시 쿠폰 최소 주문금액 조건을 더 이상 충족하지 못하면 코어가 취소 자체를 거부 (422) 하여 PG 호출이 발생하지 않습니다. KG 이니시스 API 호출이 실패하면 주문 상태 변경이 롤백됩니다.
|
||||
|
||||
## API
|
||||
|
||||
### 사용자 API
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| `POST` | `/api/plugins/sirsoft-pay_kginicis/payment/signature` | PC 결제창 서명 생성 |
|
||||
| `POST` | `/api/plugins/sirsoft-pay_kginicis/payment/mobile/signature` | 모바일 `P_CHKFAKE` 생성 |
|
||||
| `POST` | `/api/plugins/sirsoft-pay_kginicis/payment/cbt/hash-data` | CBT hashData 생성 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_kginicis/user/orders/{orderNumber}/receipt` | KG 이니시스 영수증 URL 조회 |
|
||||
|
||||
### 관리자 API
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| `GET` | `/api/plugins/sirsoft-pay_kginicis/admin/vbank-notify-url` | PC/모바일 가상계좌 입금통보 URL 조회 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_kginicis/admin/orders/test-mode-map` | 주문목록 테스트 모드 배지용 맵 조회 |
|
||||
| `POST` | `/api/plugins/sirsoft-pay_kginicis/admin/transaction/query` | TID로 KG 이니시스 거래 조회 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/transaction-status` | 주문번호로 거래 상태 조회 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/escrow-delivery` | 에스크로 배송 등록 폼 데이터 조회 |
|
||||
| `POST` | `/api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/escrow-delivery` | KG 이니시스 에스크로 배송 등록 |
|
||||
| `POST` | `/api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/escrow-deny-confirm` | 에스크로 구매거절확인 |
|
||||
| `POST` | `/api/plugins/sirsoft-pay_kginicis/admin/cbt-test-product` | CBT 테스트용 JPY 상품 생성 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_kginicis/admin/cbt-connectivity-check` | CBT 테스트 호스트 연결 진단 |
|
||||
|
||||
## 훅
|
||||
|
||||
다른 모듈이나 플러그인에서 아래 훅에 연결해 결제 흐름을 확장할 수 있습니다.
|
||||
|
||||
| 훅 | 타입 | 시점 |
|
||||
|----|------|------|
|
||||
| `sirsoft-pay_kginicis.payment.before_authorize` | action | KG 이니시스 서버 승인 API 호출 전 |
|
||||
| `sirsoft-pay_kginicis.payment.after_authorize` | action | KG 이니시스 서버 승인 완료 후 |
|
||||
| `sirsoft-pay_kginicis.payment.before_cancel` | action | KG 이니시스 결제 취소 API 호출 전 |
|
||||
| `sirsoft-pay_kginicis.payment.after_cancel` | action | KG 이니시스 결제 취소 완료 후 |
|
||||
|
||||
`sirsoft-ecommerce.payment.refund` 필터를 통해 이커머스 환불 요청을 KG 이니시스 취소/부분취소 API로 연결합니다.
|
||||
|
||||
## 보안 및 운영 참고
|
||||
|
||||
- 운영 도메인의 `APP_URL`을 HTTPS 절대 URL로 정확히 설정하세요.
|
||||
- 운영 signKey, INIAPI key, INIAPI IV, 모바일 hash key, CBT hash key는 외부에 노출하지 마세요.
|
||||
- 결제창 서명, 모바일 해시, CBT 해시 생성 요청은 타임스탬프 신선도를 검증합니다.
|
||||
- CBT 해시 생성 요청은 주문자 이메일/연락처와 서버 주문 정보를 대조하고, IP/주문번호 단위 요청 횟수를 제한합니다.
|
||||
- CBT 환불은 결제 당시 저장된 테스트/운영 모드와 일본 MID 기준으로 처리합니다.
|
||||
- PC `authUrl`, 모바일 `P_REQ_URL`, PC `netCancelUrl`은 KG 이니시스 허용 URL만 사용합니다.
|
||||
- 동일 거래번호 콜백은 중복 처리하지 않도록 방어합니다.
|
||||
- 운영 환경에서는 가상계좌 입금통보 IP 화이트리스트가 적용됩니다.
|
||||
- 가상계좌 입금통보 URL은 KG 이니시스 가맹점 관리자에 반드시 등록해야 합니다.
|
||||
- KG 이니시스 계약이 없는 결제수단이나 간편결제를 활성화하면 결제창 오류가 발생할 수 있습니다.
|
||||
- CBT 테스트가 실패하면 관리자 CBT 연결 진단에서 서버 egress IP와 `devcbt.inicis.com` 443 연결 상태를 먼저 확인하세요.
|
||||
|
||||
## 테스트
|
||||
|
||||
플러그인을 G7 프로젝트에 배치한 뒤 G7 루트에서 PHP 테스트를 실행합니다.
|
||||
|
||||
```bash
|
||||
php artisan test plugins/sirsoft-pay_kginicis/tests
|
||||
```
|
||||
|
||||
프론트엔드 테스트와 빌드는 플러그인 디렉토리에서 실행합니다.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run test:run
|
||||
npm run build
|
||||
```
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 라이선스
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# KG 이니시스 개발자 문서
|
||||
|
||||
> plugins/_bundled/sirsoft-pay_kginicis · 플러그인
|
||||
|
||||
<!-- @generated:stats START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**훅 수**: 6 · **구독 훅 수**: 14 · **라우트 수**: 35 · **모델 수**: 0 · **테이블 수**: 0 · **마이그레이션 수**: 0 · **레이아웃 수**: 1 · **핸들러 수**: 1
|
||||
<!-- @generated:stats END -->
|
||||
|
||||
## 문서 목차
|
||||
|
||||
<!-- @generated:doc-toc START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | 설계 의도·계층 지도·디렉토리 맵 |
|
||||
| [extension-points.md](extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 |
|
||||
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
|
||||
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
|
||||
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
|
||||
| [api/](api/README.md) | API 레퍼런스 |
|
||||
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
|
||||
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
|
||||
<!-- @generated:doc-toc END -->
|
||||
@@ -0,0 +1,65 @@
|
||||
# KG 이니시스 — 아키텍처
|
||||
|
||||
> 설계 의도와 계층 구조 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설계 의도
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 프로토콜(PC/모바일/CBT)마다 별도 컨트롤러·서비스 경로를 두면서도, 이커머스 쪽에는
|
||||
`sirsoft-ecommerce.payment.registered_pg_providers`/`registered_cash_receipt_providers`
|
||||
필터로 등록하는 하나의 진입점만 노출합니다. 이 경계 덕분에 KG 이니시스가 프로토콜을 바꾸거나
|
||||
새 결제수단을 추가해도 이커머스 모듈 코드는 건드리지 않습니다 — 변경은 이 플러그인 안에서만
|
||||
일어납니다. 반대로 이 플러그인이 소유 테이블을 두지 않는 것도 같은 경계 원칙입니다: 결제
|
||||
"사실"(성공/실패/금액/취소)은 이커머스가 소유하고, 이 플러그인은 그 사실을 만드는 절차만
|
||||
소유합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 계층 지도
|
||||
|
||||
<!-- @intent START -->
|
||||
```
|
||||
Controllers (PaymentCallbackController 등 — PG 콜백 수신, 화이트리스트·재처리 방지 검증)
|
||||
│
|
||||
▼
|
||||
Services (KgInicisApiService — 승인/취소/CBT API 호출, 서명·해시 생성)
|
||||
│
|
||||
├──▶ Repositories (CbtCvsOperationsRepository/CbtReconciliationRepository
|
||||
│ — sirsoft-ecommerce 의 Order 모델을 조회, 자체 테이블 없음)
|
||||
│
|
||||
└──▶ 훅 발행 (before/after_authorize·cancel·cbt_refund) ──▶ 다른 확장 리스너
|
||||
|
||||
Listeners (RegisterPgProviderListener 등 — 이커머스 레지스트리 등록,
|
||||
레이아웃 확장 주입, 설정 검증)
|
||||
```
|
||||
|
||||
미들웨어(`InicisNotifyIpWhitelist`)는 이 흐름과 별도 레인에서 가상계좌/CBT 편의점 입금통보
|
||||
라우트 앞단을 지킵니다 — Service 계층이 아니라 라우팅 계층에서 걸러야 신뢰할 수 없는 발신자의
|
||||
요청이 애초에 비즈니스 로직에 닿지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 디렉토리
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Repositories/` | 데이터 접근 | 목록 쿼리는 컬럼 프루닝·정렬 화이트리스트 확인 |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-pay_kginicis --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-pay_kginicis --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-pay_kginicis --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-pay_kginicis --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-pay_kginicis --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
@@ -0,0 +1,75 @@
|
||||
# KG 이니시스 — 데이터 모델
|
||||
|
||||
> 모델·소유 테이블·마이그레이션·Enum · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 모델
|
||||
|
||||
<!-- @generated:models START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_소유 모델이 없습니다._
|
||||
<!-- @generated:models END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 상태(주문·결제 성공/실패/취소/금액)는 전부 `sirsoft-ecommerce`의 `Order`/결제 모델에
|
||||
있습니다. 이 플러그인이 자체 모델을 두지 않는 것은 실수나 미완성이 아니라 설계입니다(§AGENTS.md
|
||||
"이 확장은 무엇인가") — PG 마다 결제 기록 테이블을 따로 두면 "이 주문이 지금 실제로 어떤
|
||||
상태인가"를 물을 때 여러 테이블을 조인해야 하고, PG 를 교체하면 과거 주문의 결제 이력을
|
||||
조회할 방법이 갈라집니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 소유 테이블
|
||||
|
||||
<!-- @generated:tables START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_소유 테이블이 없습니다._
|
||||
<!-- @generated:tables END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
KG 이니시스 고유 정보(MID·서명키 등)는 코어 `PluginSettingsService`(설정 스키마)에, 가상계좌
|
||||
계좌정보·CBT 승인 정보 같은 거래별 데이터는 이커머스 주문/결제 레코드의 JSON 컬럼 또는
|
||||
연관 필드에 함께 저장됩니다 — 이 플러그인이 그 값을 "소유"하지 않고 이커머스 테이블에
|
||||
"기록"만 남기는 형태입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 마이그레이션
|
||||
|
||||
<!-- @generated:migrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_마이그레이션이 없습니다._
|
||||
<!-- @generated:migrations END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
소유 테이블이 없으므로 마이그레이션도 없습니다. 이 플러그인이 설정 스키마를 바꿀 때는
|
||||
마이그레이션이 아니라 `config/settings/defaults.json`(§settings.md)과 필요 시 업그레이드
|
||||
스텝(과거 설정값 정정용)을 씁니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Enum
|
||||
|
||||
<!-- @generated:enums START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_Enum 이 없습니다._
|
||||
<!-- @generated:enums END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제수단·상태 분류(카드/계좌이체/가상계좌/휴대폰 등)는 이 플러그인이 아니라 이커머스가 소유한
|
||||
결제수단 Enum 을 그대로 따릅니다 — PG 마다 결제수단 이름을 다시 정의하면 이커머스가 PG 를
|
||||
교체 가능한 형태로 다룰 수 없습니다. KG 이니시스 API 고유의 코드값(예: `acceptmethod` 문자열
|
||||
조합)은 Enum 이 아니라 KG 이니시스 API 스펙에 맞춘 문자열 상수로만 존재합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Repository
|
||||
|
||||
<!-- @generated:repositories START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 클래스 | 종류 | 설명 |
|
||||
|---|---|---|
|
||||
| `CbtCvsOperationsRepository` | 구현 | - |
|
||||
| `CbtCvsOperationsRepositoryInterface` | 인터페이스 | - |
|
||||
| `CbtReconciliationRepository` | 구현 | - |
|
||||
| `CbtReconciliationRepositoryInterface` | 인터페이스 | - |
|
||||
<!-- @generated:repositories END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
두 Repository 모두 자체 테이블이 아니라 `sirsoft-ecommerce`의 `Order` 모델을 조회합니다
|
||||
(`CbtCvsOperationsRepository::findOrderWithPayment()`가 대표적 예). 소유 데이터가 없는데도
|
||||
Repository 인터페이스를 쓰는 이유는 "이 플러그인이 이커머스 데이터에 접근하는 지점"을
|
||||
Service 안에 흩어진 쿼리가 아니라 한 곳으로 모아, 나중에 이커머스의 주문 조회 방식이 바뀌어도
|
||||
이 두 클래스만 고치면 되게 하기 위해서입니다 — 일반적인 "내 테이블 CRUD" Repository 와는
|
||||
쓰임이 다릅니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,155 @@
|
||||
# KG 이니시스 — 확장점
|
||||
|
||||
> 발행/구독 훅·미들웨어·채널·스케줄 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 발행 훅
|
||||
|
||||
<!-- @generated:hooks-published START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
발행 훅 6종 / 호출 지점 6곳.
|
||||
|
||||
| 훅 이름 | 유형 | 설명 | 발행 위치 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-pay_kginicis.payment.after_authorize` | action | KG 이니시스 서버 승인 완료 후 | `src/Controllers/PaymentCallbackController.php:247` |
|
||||
| `sirsoft-pay_kginicis.payment.after_cancel` | action | KG 이니시스 결제 취소 완료 후 | `src/Services/KgInicisApiService.php:780` |
|
||||
| `sirsoft-pay_kginicis.payment.after_cbt_refund` | action | KG 이니시스 일본 CBT 결제 취소 완료 후 | `src/Services/KgInicisApiService.php:592` |
|
||||
| `sirsoft-pay_kginicis.payment.before_authorize` | action | KG 이니시스 서버 승인 API 호출 전 | `src/Controllers/PaymentCallbackController.php:243` |
|
||||
| `sirsoft-pay_kginicis.payment.before_cancel` | action | KG 이니시스 결제 취소 API 호출 전 (본인인증 등 확장 지점) | `src/Services/KgInicisApiService.php:758` |
|
||||
| `sirsoft-pay_kginicis.payment.before_cbt_refund` | action | KG 이니시스 일본 CBT 결제 취소 API 호출 전 | `src/Services/KgInicisApiService.php:564` |
|
||||
<!-- @generated:hooks-published END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
일반 결제(승인/취소)와 CBT(일본)가 각각 별도 `before/after_cbt_refund` 훅 쌍을 갖는 이유는
|
||||
두 흐름이 서로 다른 API·통화·해시 체계를 쓰기 때문입니다 — 하나로 합치면 구독자가 매번
|
||||
"이게 CBT 인지 일반인지"를 페이로드로 분기해야 합니다. `before_cancel`은 발행 위치 설명에
|
||||
"본인인증 등 확장 지점"이라고 명시돼 있습니다 — 고액 취소에 관리자 재인증을 강제하고 싶은
|
||||
확장은 이 훅에서 예외를 던져 PG 호출 자체를 막을 수 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 구독 훅
|
||||
|
||||
<!-- @generated:hooks-subscribed START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 훅 이름 | 유형 | 리스너 | 메서드 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| `core.layout_extension.after_apply` | filter | `AdjustEcommercePaymentMethodsLayoutListener` | `adjustPaymentMethodsLayout` | 20 |
|
||||
| `core.layout_extension.after_apply` | filter | `EnsureAdminOrderDetailPaymentQueryLayoutListener` | `ensurePaymentQueryLayout` | 66 |
|
||||
| `core.layout_extension.after_apply` | filter | `EnsureAdminOrderDetailTestModeLayoutListener` | `ensureTestModeLayout` | 65 |
|
||||
| `core.layout_extension.after_apply` | filter | `EnsureAdminOrderListTestBadgeLayoutListener` | `ensureTestBadgeLayout` | 60 |
|
||||
| `core.plugin_settings.before_save` | action (미선언) | `ValidateCbtSettingsListener` | `validateBeforeSave` | 10 |
|
||||
| `core.plugins.updated` | action | `RestoreLayoutExtensionsAfterUpdateListener` | `restoreCurrentExtensionsAfterUpdate` | 20 |
|
||||
| `sirsoft-ecommerce.cash_receipt.cancel` | filter | `RegisterCashReceiptProviderListener` | `cancel` | 10 |
|
||||
| `sirsoft-ecommerce.cash_receipt.issue` | filter | `RegisterCashReceiptProviderListener` | `issue` | 10 |
|
||||
| `sirsoft-ecommerce.cash_receipt.registered_providers` | filter | `RegisterCashReceiptProviderListener` | `registerProvider` | 10 |
|
||||
| `sirsoft-ecommerce.payment.get_client_config` | filter | `RegisterPgProviderListener` | `getClientConfig` | 10 |
|
||||
| `sirsoft-ecommerce.payment.refund` | filter | `CancelActivityLogListener` | `logCancelConfirmed` | 20 |
|
||||
| `sirsoft-ecommerce.payment.refund` | filter | `PaymentRefundListener` | `processRefund` | 10 |
|
||||
| `sirsoft-ecommerce.payment.registered_pg_providers` | filter | `RegisterPgProviderListener` | `registerProvider` | 10 |
|
||||
| `sirsoft-ecommerce.settings.filter_available_payment_methods` | filter | `RegisterEasyPayMethodsListener` | `injectEasyPayMethods` | 20 |
|
||||
<!-- @generated:hooks-subscribed END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`core.layout_extension.after_apply`를 구독하는 3개 리스너(`Ensure*LayoutListener`)가 서로
|
||||
다른 우선순위(60/65/66)를 갖는 것은 우연이 아닙니다 — 관리자 주문 목록/상세 레이아웃에 여러
|
||||
확장이 조각을 주입할 수 있어, 이 플러그인의 조각들이 서로 겹치지 않는 순서로 배치되도록
|
||||
번호를 나눠 씁니다. `sirsoft-ecommerce.payment.refund`를 구독하는 두 리스너의 우선순위
|
||||
(`PaymentRefundListener`=10, `CancelActivityLogListener`=20)는 §AGENTS.md 핵심 흐름에서
|
||||
설명한 대로 "취소 성공 후에만 로그 기록"을 강제하기 위한 순서입니다 — 뒤바뀌면 실패한 취소도
|
||||
로그에 성공처럼 남을 수 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 훅 리스너
|
||||
|
||||
<!-- @generated:listeners START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 리스너 | 구독 훅 | 등록 방식 | HookListenerInterface | 파일 |
|
||||
|---|---|---|---|---|
|
||||
| `AdjustEcommercePaymentMethodsLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/AdjustEcommercePaymentMethodsLayoutListener.php` |
|
||||
| `CancelActivityLogListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/CancelActivityLogListener.php` |
|
||||
| `EnsureAdminOrderDetailPaymentQueryLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/EnsureAdminOrderDetailPaymentQueryLayoutListener.php` |
|
||||
| `EnsureAdminOrderDetailTestModeLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/EnsureAdminOrderDetailTestModeLayoutListener.php` |
|
||||
| `EnsureAdminOrderListTestBadgeLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/EnsureAdminOrderListTestBadgeLayoutListener.php` |
|
||||
| `PaymentRefundListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/PaymentRefundListener.php` |
|
||||
| `RegisterCashReceiptProviderListener` | 3개 | 명시 등록 | ✅ | `src/Listeners/RegisterCashReceiptProviderListener.php` |
|
||||
| `RegisterEasyPayMethodsListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/RegisterEasyPayMethodsListener.php` |
|
||||
| `RegisterPgProviderListener` | 2개 | 명시 등록 | ✅ | `src/Listeners/RegisterPgProviderListener.php` |
|
||||
| `RestoreLayoutExtensionsAfterUpdateListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/RestoreLayoutExtensionsAfterUpdateListener.php` |
|
||||
| `ValidateCbtSettingsListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/ValidateCbtSettingsListener.php` |
|
||||
<!-- @generated:listeners END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`RegisterPgProviderListener`·`RegisterCashReceiptProviderListener`·`RegisterEasyPayMethodsListener`
|
||||
가 이 플러그인의 "등록" 축입니다 — 이 셋이 없으면 플러그인을 활성화해도 이커머스 화면에서
|
||||
KG 이니시스가 보이지 않습니다. 나머지는 전부 부가 UI(`Ensure*LayoutListener`)나 정합성
|
||||
검증(`ValidateCbtSettingsListener`)입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 레이아웃 확장
|
||||
|
||||
<!-- @generated:layout-extensions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 대상 | 설명 |
|
||||
|---|---|
|
||||
| `resources/extensions/admin_order_list_test_badge.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/admin_order_payment_query.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/checkout_payment_error.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/user_order_show.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
<!-- @generated:layout-extensions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`checkout_payment_error.json`은 체크아웃 화면에 KG 이니시스 특유의 오류 메시지(예: 계약되지
|
||||
않은 결제수단 선택 시 안내)를 끼워 넣고, `user_order_show.json`은 주문 상세에 영수증 버튼을
|
||||
추가합니다. 관리자 쪽 두 조각(`admin_order_list_test_badge`/`admin_order_payment_query`)은
|
||||
"이 주문이 테스트 모드로 결제됐는가"와 "PG 거래 상태를 다시 조회"를 관리자 화면에서 바로
|
||||
확인하게 하는 운영 편의 기능입니다 — 실제 결제 로직과는 분리돼 있어 이 조각만 비활성화해도
|
||||
결제 자체는 영향받지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 미들웨어
|
||||
|
||||
<!-- @generated:middleware START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 미들웨어 | 부착 대상(targets) | 우선순위 |
|
||||
|---|---|---|
|
||||
| `InicisNotifyIpWhitelist` | `web.plugins.sirsoft-pay_kginicis.payment.cbt.cvs-notify`, `web.plugins.sirsoft-pay_kginicis.payment.vbank-notify`, `web.plugins.sirsoft-pay_kginicis.payment.mobile.vbank-notify` | - |
|
||||
<!-- @generated:middleware END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
가상계좌/CBT 편의점 입금통보 3개 라우트에만 부착되고 결제 승인/취소 콜백 라우트에는
|
||||
부착되지 않습니다 — 입금통보는 KG 이니시스 서버가 발신자 인증 수단 없이 단순 POST 로
|
||||
호출하므로 IP 로 걸러야 하지만, 승인/취소 콜백은 `authUrl`/`authToken` 자체가 위조 방지
|
||||
수단(§AGENTS.md 금지 패턴)이라 별도 IP 제한이 없어도 안전합니다. `local`/`testing` 환경에서는
|
||||
이 제한이 우회됩니다 — 개발 중에는 실제 KG 이니시스 IP 대역에서 요청이 오지 않기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 브로드캐스트 채널
|
||||
|
||||
<!-- @generated:channels START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 브로드캐스트 채널이 없습니다._
|
||||
<!-- @generated:channels END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 진행 상황(승인 대기 등)을 실시간으로 밀어줄 필요가 없습니다 — PC/모바일 결제는 결제창이
|
||||
닫히고 콜백이 오는 시점에 화면이 이미 그 페이지에 있고, 가상계좌 입금통보는 방문자가 화면을
|
||||
보고 있지 않은 시점에 도착하므로 알림(§알림 정의)이나 다음 방문 시 조회가 더 적절합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 스케줄
|
||||
|
||||
<!-- @generated:schedules START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 스케줄이 없습니다._
|
||||
<!-- @generated:schedules END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
가상계좌 만료 처리나 CBT 정산 대사(reconciliation) 같은 주기적 점검이 있을 법하지만
|
||||
(`CbtReconciliationRepository` 참고), 현재는 관리자가 필요할 때 수동으로 조회·확인하는
|
||||
구조입니다 — 자동 스케줄로 상태를 바꾸면 결제 상태 변경 시점을 운영자가 놓칠 수 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 알림 정의
|
||||
|
||||
<!-- @generated:notifications START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 알림 정의가 없습니다._
|
||||
<!-- @generated:notifications END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 완료/실패 알림은 이 플러그인이 아니라 `sirsoft-ecommerce`의 주문 상태 알림 정의가
|
||||
담당합니다 — PG 가 여러 개일 수 있는데 PG 마다 "결제 완료 알림"을 각자 만들면 같은 이벤트에
|
||||
대해 서로 다른 알림 정의가 난립합니다. 이 플러그인은 "그 결제가 KG 이니시스를 통했다"는
|
||||
사실만 이커머스에 전달하고, 알림 발송은 이커머스가 단일하게 책임집니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,81 @@
|
||||
# KG 이니시스 — 프론트엔드
|
||||
|
||||
> 레이아웃·액션 핸들러·전역 진입점·에셋 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 레이아웃
|
||||
|
||||
<!-- @generated:layouts START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
레이아웃 1개 (루트: `resources/layouts`).
|
||||
|
||||
| 그룹 | 개수 |
|
||||
|---|---|
|
||||
| `admin` | 1개 |
|
||||
|
||||
| 레이아웃 | 그룹 | 종류 | extends |
|
||||
|---|---|---|---|
|
||||
| `plugin_settings` | `admin` | 화면 | `_admin_base` |
|
||||
<!-- @generated:layouts END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
이 플러그인이 소유한 화면 레이아웃은 관리자 설정 화면 하나뿐입니다 — 체크아웃·주문상세의
|
||||
결제 UI는 이 플러그인 소유가 아니라 §레이아웃 확장(다른 확장/템플릿 레이아웃에 주입되는
|
||||
조각)으로 존재합니다. "화면"과 "레이아웃 확장 조각"을 헷갈리면 체크아웃 결제 버튼을 찾으러
|
||||
`resources/layouts/`를 뒤지게 되는데, 실제로는 `resources/extensions/`에 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 액션 핸들러
|
||||
|
||||
<!-- @generated:handlers START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
핸들러 1개 (정의: `resources/js/handlers/index.ts`).
|
||||
|
||||
| 핸들러 | 레이아웃에서 부르는 이름 |
|
||||
|---|---|
|
||||
| `requestPayment` | `sirsoft-pay_kginicis.requestPayment` |
|
||||
<!-- @generated:handlers END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`requestPayment` 하나로 PC/모바일/CBT 3가지 프로토콜을 전부 처리합니다 — 체크아웃 버튼은
|
||||
결제수단·통화가 무엇이든 이 핸들러 하나만 호출하고, PC 결제창을 열지 모바일 폼을 제출할지
|
||||
CBT 인증 URL로 이동할지는 핸들러 내부에서 서버 응답(§API `/payment/*/signature`,
|
||||
`/payment/cbt/hash-data`)에 따라 분기합니다. 레이아웃 JSON 작성자가 결제수단별로 다른
|
||||
핸들러를 호출할 필요가 없다는 뜻입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 전역 진입점
|
||||
|
||||
<!-- @generated:frontend-entry START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 엔트리 파일 | `resources/js/index.ts` |
|
||||
| 전역 객체 | `window.__SirsoftKginicis` |
|
||||
| 재등록 진입점 | `initPlugin()` |
|
||||
|
||||
로케일 전환 시 코어가 이 진입점을 호출해 핸들러를 다시 등록합니다. 진입점은 핸들러 재등록만 수행하고 1회성 부팅 작업을 포함하지 않습니다.
|
||||
<!-- @generated:frontend-entry END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`window.__SirsoftKginicis` 로 노출되는 이유는 코어가 로케일 전환 시 이 이름으로 재등록
|
||||
진입점을 찾기 때문입니다(§CLAUDE.md "재등록 진입점"). `initPlugin()`이 KG 이니시스 결제창
|
||||
스크립트(`INIStdPay.js`) 자체를 미리 로드하지 않는 것도 의도입니다 — 그 스크립트는
|
||||
`requestPayment` 핸들러가 실제 결제 시도 시점에만 동적으로 로드합니다(모든 방문자가 결제
|
||||
페이지에 오는 것은 아니므로 전역 부팅에서 미리 불러올 필요가 없습니다).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 에셋
|
||||
|
||||
<!-- @generated:assets START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 구분 |
|
||||
|---|---|
|
||||
| `dist/js/plugin.iife.js` | 빌드 산출물 (커밋 대상) |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 (manifest) |
|
||||
|
||||
로딩 설정: `{"strategy":"global","priority":100,"dependencies":[]}`
|
||||
<!-- @generated:assets END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
KG 이니시스가 제공하는 `INIStdPay.js`/모바일 결제창 스크립트는 이 목록에 없습니다 — 그
|
||||
스크립트들은 KG 이니시스 CDN 에서 결제 시도 시점에 동적으로 로드되는 제3자 자산이라, 이
|
||||
플러그인이 빌드 시 번들링하는 `dist/` 산출물과는 다른 층입니다. CSS 산출물이 없는 것은
|
||||
결제창 자체는 KG 이니시스가 그리고, 이 플러그인은 결제 버튼 같은 최소한의 UI만 코어
|
||||
컴포넌트로 구성하기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,119 @@
|
||||
# KG 이니시스 — 설정·권한·라우트
|
||||
|
||||
> 설정 스키마·권한·메뉴·라우트·의존 관계 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설정 스키마
|
||||
|
||||
<!-- @generated:settings-schema START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 타입 | 기본값 | 설명 |
|
||||
|---|---|---|---|
|
||||
| `is_test_mode` | `boolean` | `true` | 테스트 모드 |
|
||||
| `test_mid` | `string` | `INIpayTest` | 테스트 가맹점 ID (MID) |
|
||||
| `test_sign_key` | `string` | `SU5JTElURV9UUklQTEVERVNfS0VZU1RS` | 테스트 사인키 |
|
||||
| `test_iniapi_key` | `string` | `ItEQKi3rY7uvDS8l` | 테스트 INIAPI 키 |
|
||||
| `test_iniapi_iv` | `string` | `HYb3yQ4f65QL89==` | 테스트 INIAPI IV |
|
||||
| `live_mid` | `string` | - | 라이브 가맹점 ID (MID) |
|
||||
| `live_sign_key` | `string` | - | 라이브 사인키 |
|
||||
| `live_iniapi_key` | `string` | - | 라이브 INIAPI 키 |
|
||||
| `live_iniapi_iv` | `string` | - | 라이브 INIAPI IV |
|
||||
| `test_mobile_hash_key` | `string` | `3CB8183A4BE283555ACC8363C0360223` | 테스트 모바일 해시키 |
|
||||
| `live_mobile_hash_key` | `string` | - | 라이브 모바일 해시키 |
|
||||
| `use_escrow` | `boolean` | `false` | 에스크로 결제 활성화 |
|
||||
| `japan_enabled` | `boolean` | `false` | 일본 결제 활성화 |
|
||||
| `japan_restrict_jpy_payment_methods` | `boolean` | `false` | JPY 주문 결제수단 제한 |
|
||||
| `test_japan_sign_key` | `string` | `5AL5Djb1Ipualn0F` | 테스트 일본 CBT 해시키 |
|
||||
| `live_japan_mid` | `string` | - | 라이브 일본 MID |
|
||||
| `live_japan_sign_key` | `string` | - | 라이브 일본 CBT 해시키 |
|
||||
| `japan_merchant_name` | `string` | `サンプルストア` | 일본 결제 가맹점명 |
|
||||
| `japan_merchant_name_kana` | `string` | `サンプルストア` | 일본 결제 가맹점명 Kana |
|
||||
| `japan_merchant_name_alphabet` | `string` | `Sample Store` | 일본 결제 가맹점명 영문 |
|
||||
| `japan_merchant_name_short` | `string` | `サンプル` | 일본 결제 가맹점 약칭 |
|
||||
| `japan_contact_name` | `string` | `サポート窓口` | 일본 결제 문의처명 |
|
||||
| `japan_contact_email` | `string` | `support@example.com` | 일본 결제 문의 이메일 |
|
||||
| `japan_contact_phone` | `string` | `0120-123-456` | 일본 결제 문의 전화번호 |
|
||||
| `japan_contact_opening_hours` | `string` | `10:00-18:00` | 일본 결제 문의 영업시간 |
|
||||
| `redirect_success_url` | `string` | `{shopBase}/orders/{orderId}/complete` | 결제 성공 리다이렉트 URL |
|
||||
| `redirect_fail_url` | `string` | `{shopBase}/checkout` | 결제 실패 리다이렉트 URL |
|
||||
| `easy_pay_allow_with_other_pg` | `boolean` | `false` | 타 PG와 사용가능함 |
|
||||
| `easy_pay_samsung_pay` | `boolean` | `false` | KG이니시스 삼성페이 사용 |
|
||||
| `easy_pay_naverpay` | `boolean` | `false` | KG이니시스 네이버페이 사용 |
|
||||
| `easy_pay_show_brand_button` | `boolean` | `false` | 간편결제 브랜드 버튼 표시 |
|
||||
| `easy_pay_lpay` | `boolean` | `false` | KG이니시스 L.pay 사용 |
|
||||
| `easy_pay_kakaopay` | `boolean` | `false` | KG이니시스 카카오페이 사용 |
|
||||
| `use_credit_point` | `boolean` | `false` | 신용카드 포인트 사용 |
|
||||
|
||||
기본값 파일: `config/settings/defaults.json` · 설정 화면 레이아웃: `resources/layouts/admin/plugin_settings.json`
|
||||
<!-- @generated:settings-schema END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`test_*`/`live_*` 접두어 쌍이 반복되는 것이 이 스키마의 핵심 구조입니다 — 테스트 모드와
|
||||
운영 모드가 완전히 다른 자격증명 집합을 쓰기 때문에, 하나의 키를 두고 모드에 따라 값을
|
||||
바꾸는 대신 애초에 별도 키로 분리했습니다. 이 덕분에 `is_test_mode` 를 껐다 켰다 해도 각
|
||||
모드의 자격증명은 서로 덮어쓰이지 않습니다. 일본(`japan_*`) 설정군이 특히 많은 이유는
|
||||
KG 이니시스 CBT 결제창의 `extraData`(가맹점 표시 정보)가 한국 표준결제와 별개의 계약·심사
|
||||
단위이기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 권한
|
||||
|
||||
<!-- @generated:permissions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_선언된 권한이 없습니다._
|
||||
<!-- @generated:permissions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 설정은 이커머스의 관리자 권한 체계 안에서 다뤄집니다 — "결제 설정을 볼 수 있는 사람"은
|
||||
PG 마다 다시 정의할 이유가 없는 하나의 개념이라, 이 플러그인이 별도 권한을 선언하지 않고
|
||||
이커머스의 결제/설정 권한에 얹혀 갑니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 메뉴
|
||||
|
||||
<!-- @generated:menus START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 메뉴가 없습니다._
|
||||
<!-- @generated:menus END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
설정 화면(`plugin_settings.json`)은 코어의 "플러그인 관리 > 설정" 공통 진입점을 통해
|
||||
접근합니다 — PG 플러그인마다 전용 사이드바 메뉴를 만들면 PG 를 여러 개 설치했을 때 메뉴가
|
||||
난립합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 라우트
|
||||
|
||||
<!-- @generated:routes START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 파일 | URL prefix |
|
||||
|---|---|---|
|
||||
| `api` | `src/routes/api.php` | `/api/plugins/sirsoft-pay_kginicis/...` |
|
||||
| `web` | `src/routes/web.php` | `/plugins/sirsoft-pay_kginicis/...` |
|
||||
|
||||
확장 라우트는 **활성 상태인 확장의 것만** 등록됩니다. 라우트 정의를 바꾸면 라우트 캐시 재생성이 필요합니다.
|
||||
<!-- @generated:routes END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`api`(Bearer 토큰 인증, 결제창 서명·해시 생성처럼 로그인 사용자가 브라우저에서 직접 호출하는
|
||||
엔드포인트)와 `web`(콜백·입금통보처럼 KG 이니시스 서버나 리다이렉트로 도달하는 엔드포인트)이
|
||||
분리된 이유는 인증 방식이 다르기 때문입니다 — KG 이니시스는 우리 서비스의 Bearer 토큰을 모르므로
|
||||
콜백 라우트에 `api` 인증 미들웨어를 걸 수 없습니다. 새 KG 이니시스 콜백을 추가할 때는 `web`
|
||||
쪽에, 프론트엔드가 로그인 상태로 직접 호출하는 기능은 `api` 쪽에 둡니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 의존 관계
|
||||
|
||||
<!-- @generated:dependencies START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
| 확장 | 유형 | 버전 제약 | 번들 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-ecommerce` | 모듈 | `>=1.1.0` | ✅ |
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
없음.
|
||||
<!-- @generated:dependencies END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`sirsoft-ecommerce >=1.1.0` 하드 의존은 §data-model.md 에서 설명한 구조(결제 상태는 이커머스가
|
||||
소유, 이 플러그인은 절차만 소유)의 직접적 결과입니다 — 이커머스 없이는 이 플러그인이 다룰
|
||||
주문 자체가 존재하지 않습니다. 이커머스의 PG 등록 훅(`registered_pg_providers` 등)이나
|
||||
`Order` 모델 구조가 바뀌면 이 최소 버전을 올려야 합니다(§CLAUDE.md "확장 → 확장 동기화").
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,177 @@
|
||||
# NHN KCP — 에이전트 가이드
|
||||
|
||||
> 이 문서는 이 플러그인을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 [README.md](README.md) 를 보세요.
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 유형: 플러그인 (sirsoft-pay_nhnkcp) — NHN KCP PG 연동(PC CLI 승인/모바일 SOAP/가상계좌/에스크로). 소유 테이블 없음 — 상태는 sirsoft-ecommerce 소유
|
||||
2. 확장 방식: `RegisterPgProviderListener`/`RegisterEasyPayMethodsListener` 로 이커머스 레지스트리에 등록 — 이커머스 코드는 이 플러그인을 모른다
|
||||
3. 건드리면 안 되는 것: KCP CLI(`bin/pp_cli*`) 호출 인자 사전검증(`assertSafeCliValue`) 생략, 동일 거래번호 콜백 재처리 방지 우회, IP 화이트리스트 미들웨어(`RestrictKcpIp`) 미부착
|
||||
4. 작업 위치: `plugins/_bundled/sirsoft-pay_nhnkcp` — 활성 디렉토리 직접 수정 금지
|
||||
5. 반영: `php artisan plugin:update sirsoft-pay_nhnkcp --force`
|
||||
```
|
||||
|
||||
## 1. 이 확장은 무엇인가
|
||||
|
||||
<!-- @intent START -->
|
||||
NHN KCP PG(결제 게이트웨이)를 `sirsoft-ecommerce`에 연결하는 어댑터입니다. PC 결제는 KCP CLI
|
||||
바이너리(`bin/pp_cli*`)를 서버에서 직접 실행해 승인 응답을 받고, 모바일 결제는 SOAP
|
||||
승인키 발급 후 모바일 결제창으로 폼 이동합니다. 두 프로토콜 모두 `sirsoft-pay_kginicis`(HTTP
|
||||
API 호출)와 달리 **로컬 프로세스 실행**을 최종 승인 수단으로 쓴다는 점이 이 플러그인 고유의
|
||||
설계 축입니다.
|
||||
|
||||
**설계 원칙**: 이 플러그인도 `sirsoft-pay_kginicis`와 마찬가지로 상태를 소유하지 않습니다
|
||||
(§data-model.md — 모델·테이블·Repository 0개, kginicis 의 CBT 정산 Repository 2개조차 없음:
|
||||
이 플러그인은 일본/CBT 결제를 아예 구현하지 않기 때문입니다). 등록은 훅 기반입니다
|
||||
(`sirsoft-ecommerce.payment.registered_pg_providers` 필터) — 이커머스 모듈은 이 플러그인의
|
||||
존재를 컴파일 타임에 몰라도 됩니다.
|
||||
|
||||
**의도적으로 하지 않는 것**: CLI 실행 권한이 사라진 경우(예: `plugin:update` 가 `_bundled` 의
|
||||
0664 권한을 활성 디렉토리로 그대로 복사) 조용히 실패하지 않고 결제 hot path 에서
|
||||
`ensureCliExecutable()` 로 0755 자가 복구를 시도합니다 — "결제 버튼을 눌렀는데 원인 불명으로
|
||||
9502 오류"라는 상태를 막기 위함입니다. 또한 CLI 인자에 위험 문자·제어문자가 섞이면 그 값을
|
||||
정제해서 통과시키지 않고 `NhnKcpApiException` 으로 즉시 거부합니다 — 부분 정제는 안전하다는
|
||||
착각을 주면서 실제로는 우회 경로를 남길 수 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 2. 디렉토리 지도
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-pay_nhnkcp --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-pay_nhnkcp --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-pay_nhnkcp --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-pay_nhnkcp --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-pay_nhnkcp --force` |
|
||||
| `bin/` | 확장이 실행하는 외부 바이너리·인증서 | 교체 시 OS별 파일과 권한을 함께 확인 (비면 해당 기능 정지) |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
|
||||
## 3. 핵심 흐름
|
||||
|
||||
<!-- @intent START -->
|
||||
**PC 결제 승인**: `PaymentCallbackController`(KCP 결제창이 POST 하는 `enc_data`/`enc_info`
|
||||
수신) → `sirsoft-pay_nhnkcp.payment.before_confirm` 훅 → `NhnKcpApiService::executeCli()` 가
|
||||
OS 판별 후 `executeCliWindows()`/`executeCliLinux()` 로 분기 → CLI 인자 전량
|
||||
`assertSafeCliValue()` 사전검증 → `escapeshellarg()` 이중 quoting 후 `exec()` 실행 →
|
||||
`sirsoft-pay_nhnkcp.payment.after_confirm` 훅 → 이커머스 주문 결제 완료 처리.
|
||||
`PreventsReplayCallback` 트레이트가 콜백 진입 시점에 동일 `transaction_id` 가 이미 `paid`
|
||||
상태인지 먼저 확인해 재처리를 조기 차단합니다.
|
||||
|
||||
**모바일 결제 승인**: `/mobile/approval-key` API 호출 → KCP SOAP `approve` 로 승인키·`pay_url`
|
||||
획득 → 브라우저가 `pay_url` 로 폼 POST → KCP가 `/payment/callback` 으로 결과 POST(PC와 동일
|
||||
콜백 엔드포인트 공유) → 이후는 PC 흐름과 합류.
|
||||
|
||||
**가상계좌 입금통보 / 에스크로 공통통보**: KCP 서버가 `/payment/vbank-notify` 또는
|
||||
`/payment/escrow-common-notify` 를 직접 호출 → `RestrictKcpIp` 미들웨어가 운영 모드에서
|
||||
발신 IP 를 화이트리스트와 대조 → `EscrowCommonNotifyController` 가 `tx_cd`/`cl_status` 조합으로
|
||||
4가지 훅(`escrow.purchase_confirmed`/`purchase_cancelled`/`denial_confirmed`/
|
||||
`delivery_started`) 중 하나를 분기 발화. 결제 취소는 `PaymentRefundListener`(우선순위 10)가
|
||||
먼저 KCP 취소 API 를 호출한 뒤 `CancelActivityLogListener`(우선순위 20)가 그 결과를 활동
|
||||
로그에 별도 기록합니다 — `sirsoft-pay_kginicis` 와 동일한 순서 원칙입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 4. 확장점
|
||||
|
||||
<!-- @generated:extension-points-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 확장점 | 수 | 상세 |
|
||||
|---|---|---|
|
||||
| 발행 훅 | 8개 | [발행 훅](docs/extension-points.md#발행-훅) |
|
||||
| 구독 훅 | 9개 | [구독 훅](docs/extension-points.md#구독-훅) |
|
||||
| 훅 리스너 | 8개 | [훅 리스너](docs/extension-points.md#훅-리스너) |
|
||||
| 레이아웃 확장 | 5개 | [레이아웃 확장](docs/extension-points.md#레이아웃-확장) |
|
||||
| 미들웨어 | 1개 | [미들웨어](docs/extension-points.md#미들웨어) |
|
||||
| 브로드캐스트 채널 | 0개 | [브로드캐스트 채널](docs/extension-points.md#브로드캐스트-채널) |
|
||||
| 스케줄 | 0개 | [스케줄](docs/extension-points.md#스케줄) |
|
||||
| 알림 정의 | 0개 | [알림 정의](docs/extension-points.md#알림-정의) |
|
||||
<!-- @generated:extension-points-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`before_confirm`/`before_cancel` 은 PG(CLI/SOAP) 호출 **전** 개입 지점입니다 — 예를 들어
|
||||
`before_cancel` 을 잡아 조건 미충족 시 예외를 던지면 KCP 취소 API 자체가 호출되지 않습니다.
|
||||
`after_*` 훅은 응답을 받은 뒤 부가효과를 붙이는 자리입니다. 에스크로 훅 4종은 KCP 공통통보의
|
||||
`tx_cd`/`cl_status` 조합을 이미 해석해 발화하므로, 구독하는 확장은 원시 통보 파라미터를
|
||||
다시 파싱할 필요가 없습니다. 구독 훅의 `core.layout_extension.after_apply` 3건은 관리자
|
||||
주문 목록/상세에 "테스트 모드 배지"·"거래 조회 UI"를 레이아웃 확장으로 주입하기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 5. 수정 시 동반 의무
|
||||
|
||||
- [ ] `_bundled` 에서만 수정하고 `php artisan plugin:update sirsoft-pay_nhnkcp --force` 로 반영
|
||||
- [ ] manifest version 상향 시 `package.json` · `package-lock.json` · `composer.json` 동기화 + CHANGELOG 기재
|
||||
- [ ] 발행 훅 추가·이름 변경 시 `php artisan ext:docgen` 재실행 (구독하는 확장의 계약이 바뀝니다)
|
||||
- [ ] API 표면 변경 시 `php artisan api:docgen --scope=plugin:sirsoft-pay_nhnkcp` 재실행 + `docs/api/**` 갱신
|
||||
- [ ] 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
|
||||
- [ ] TSX/TS 변경 시 `--production` 재빌드 후 `dist/` 커밋 (sourceMappingURL 잔존 금지)
|
||||
- [ ] 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
|
||||
- [ ] CLI 인자 조립부를 고칠 때 `assertSafeCliValue()` 검증을 모든 인자에 유지 — 인자 하나만 빠져도 그 필드가 injection 통로가 된다
|
||||
- [ ] `bin/` 바이너리(OS별 CLI·`pub.key`·WSDL) 교체 시 실행 권한(0755)과 파일 존재를 관리자 설정 화면의 시스템 점검(§API)으로 확인
|
||||
- [ ] 승인/취소 흐름을 고칠 때 `before_*`/`after_*` 훅 순서와 우선순위(`PaymentRefundListener` < `CancelActivityLogListener`)를 유지 — 로그가 실제 처리보다 먼저 실행되면 안 된다
|
||||
- [ ] IP 화이트리스트(`RestrictKcpIp`) 대상 라우트를 추가/변경하면 미들웨어 부착 대상(targets)도 함께 갱신
|
||||
- [ ] 새 결제수단·통화를 추가하면 그 결제수단의 콜백 URL을 관리자 설정 안내(README "콜백 및 통보 URL")에도 반영
|
||||
|
||||
## 6. 금지 패턴
|
||||
|
||||
<!-- @intent START -->
|
||||
| 금지 | 올바른 사용 | 이유 |
|
||||
|---|---|---|
|
||||
| KCP CLI 인자(`site_cd`/`tx_cd`/`enc_data` 등)를 검증 없이 문자열 결합해 `exec()` 에 전달 | `assertSafeCliValue()` 로 위험 문자·제어문자 사전 거부 후 `escapeshellarg()` 로 quoting | 검증을 생략하면 서버가 받은 KCP 응답 값이 그대로 셸 명령 인자가 되어 명령 삽입(command injection)으로 이어질 수 있다 |
|
||||
| CLI 실행 권한 오류(9502)를 그대로 사용자에게 노출하고 자가 복구를 생략 | `ensureCliExecutable()` 로 결제 hot path 진입 시 0755 자가 복구 시도 | `plugin:update` 가 파일 권한을 0664 로 되돌리는 것은 배포 절차의 부작용이지 운영자 실수가 아니다 — 매 결제 실패로 드러나게 두면 안 된다 |
|
||||
| 동일 `transaction_id` 콜백을 매번 재처리 | `PreventsReplayCallback::wasAlreadyPaid()` 로 이미 `paid` 상태면 멱등 응답 | KCP 서버의 재전송·사용자의 새로고침으로 같은 콜백이 두 번 오면 결제완료 알림·마일리지가 중복 적립될 수 있다 |
|
||||
| 에스크로 공통통보의 `tx_cd`/`cl_status` 매핑을 컨트롤러 밖(리스너 등)에서 다시 판정 | `EscrowCommonNotifyController` 의 매핑표(§핵심 흐름)를 SSoT 로 유지 | 판정 로직이 두 곳에 있으면 KCP 가 새 `cl_status` 값을 보낼 때 한쪽만 갱신되어 조용히 어긋난다 |
|
||||
| 라이브 사이트 키(`live_site_key`)를 로그·에러 메시지에 노출 | 운영 키는 항상 마스킹하거나 로그 대상에서 제외 | 노출되면 제3자가 결제창 요청을 위조할 수 있다 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 7. 테스트 실행
|
||||
|
||||
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 개수 | 위치 |
|
||||
|---|---|---|
|
||||
| PHPUnit | 28개 | `plugins/_bundled/sirsoft-pay_nhnkcp/tests` |
|
||||
| Vitest | 8개 | `vitest.config.ts` |
|
||||
| Playwright | 0개 | — |
|
||||
| 시나리오 매니페스트 | 1개 | `tests/scenarios` |
|
||||
|
||||
기저 TestCase: `tests/PluginTestCase.php` — 확장 테스트는 이 클래스를 상속합니다 (`Tests\TestCase` 직접 상속 금지).
|
||||
|
||||
```bash
|
||||
# PHPUnit (변경 범위만) (Bash)
|
||||
php vendor/bin/phpunit plugins/_bundled/sirsoft-pay_nhnkcp/tests --filter='<대상클래스>'
|
||||
|
||||
# Vitest (확장 디렉토리에서) (PowerShell)
|
||||
cd plugins/_bundled/sirsoft-pay_nhnkcp && powershell -Command "npm run test:run -- <대상>"
|
||||
|
||||
```
|
||||
|
||||
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
|
||||
<!-- @generated:test-commands END -->
|
||||
|
||||
## 8. 문서 목차
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
@@ -1,30 +1,96 @@
|
||||
# NHN KCP Plugin for G7
|
||||
# NHN KCP
|
||||
|
||||
NHN KCP Standard Pay 결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다.
|
||||
**G7 플러그인 · sirsoft-pay_nhnkcp**
|
||||
NHN KCP Standard Pay 결제를 sirsoft-ecommerce 에 연결하는 결제 플러그인
|
||||
|
||||
PC 결제는 KCP `payplus_web.jsp` 결제창과 KCP CLI 승인 모듈을 사용하고, 모바일 결제는 SmartPhone Pay SOAP 승인키를 받은 뒤 KCP 모바일 결제창으로 이동합니다.
|
||||
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-1.0.3-0066FF?style=flat-square" alt="version 1.0.3">
|
||||
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
|
||||
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 >=7.0.10">
|
||||
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
|
||||
<img src="https://img.shields.io/badge/requires-sirsoft--ecommerce-BF8700?style=flat-square" alt="requires sirsoft-ecommerce">
|
||||
</p>
|
||||
<!-- @generated:badges END -->
|
||||
|
||||
---
|
||||
|
||||
[소개](#소개) · [주요 기능](#주요-기능) · [동작 방식](#동작-방식) · [요구 사항](#요구-사항) · [설치](#설치) · [관리자 설정](#관리자-설정) · [사용 방법](#사용-방법) · [다른 확장과의 연동](#다른-확장과의-연동) · [문서](#문서) · [트러블슈팅](#트러블슈팅) · [변경 이력](#변경-이력) · [라이선스](#라이선스)
|
||||
|
||||
---
|
||||
|
||||
## 소개
|
||||
|
||||
<!-- @intent START -->
|
||||
NHN KCP Standard Pay 결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다. PC
|
||||
결제는 `payplus_web.jsp` 결제창 + 서버의 KCP CLI 승인 모듈을, 모바일 결제는 SmartPhone Pay
|
||||
SOAP 승인키 발급 + 모바일 결제창을 씁니다.
|
||||
|
||||
`sirsoft-pay_kginicis`와 마찬가지로 이 플러그인은 결제 자체의 상태(주문·결제 성공/실패/취소)를
|
||||
소유하지 않습니다 — 그 상태는 `sirsoft-ecommerce`의 주문·결제 테이블에 있고, 이 플러그인은
|
||||
"그 상태를 KCP CLI/SOAP API 와 어떻게 주고받는가"만 책임집니다(§data-model.md). 다른 PG
|
||||
플러그인과 구별되는 이 플러그인만의 특징은 PC 결제 최종 승인이 HTTP API 호출이 아니라
|
||||
**서버에서 실행하는 CLI 바이너리**라는 점입니다 — KCP 가 표준결제 승인 로직을 컴파일된
|
||||
실행파일로만 배포하기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 주요 기능
|
||||
|
||||
- 신용카드, 계좌이체, 가상계좌, 휴대폰결제 지원
|
||||
- PC Standard Pay 결제창 연동
|
||||
- 모바일 SmartPhone Pay 승인키 발급 및 모바일 결제창 연동
|
||||
- PAYCO, 네이버페이, 네이버페이 포인트, 카카오페이, Apple Pay 간편결제 버튼 주입
|
||||
- 가상계좌 발급, 입금통보, 테스트 모드 모의입금 처리
|
||||
- 에스크로 결제, 에스크로 배송 등록, 공통통보 처리
|
||||
- 결제 취소 및 부분취소 연동
|
||||
- PG 측 결제 취소 확인 시점의 활동 로그 별도 기록 (PG 응답 시각·취소 거래번호 사후 추적)
|
||||
- 주문 완료/마이페이지 영수증, 현금영수증 조회 버튼 주입
|
||||
- 관리자 주문 상세의 KCP 거래 정보 표시
|
||||
- 관리자 설정 화면의 KCP 실행 환경 점검
|
||||
<!-- @intent START -->
|
||||
| 영역 | 설명 |
|
||||
|---|---|
|
||||
| 결제수단 | 신용카드, 계좌이체, 가상계좌, 휴대폰결제 |
|
||||
| 간편결제 | PAYCO, 네이버페이, 네이버페이 포인트, 카카오페이, Apple Pay 버튼 주입 |
|
||||
| PC 결제 | `payplus_web.jsp` 표준결제창 + 서버 KCP CLI 승인 |
|
||||
| 모바일 결제 | SmartPhone Pay SOAP 승인키 발급 + 모바일 결제창 |
|
||||
| 가상계좌 | 발급, 입금통보, 테스트 모드 모의입금 |
|
||||
| 에스크로 | 결제, 배송 등록, 공통통보(구매확인/구매취소/구매취소확인/배송시작) |
|
||||
| 결제 취소 | 전액/부분취소, PG 취소 확인 시점 별도 활동 로그(PG 응답 시각·취소 거래번호) |
|
||||
| 영수증 | 주문 완료/마이페이지 영수증, 현금영수증 조회 버튼 |
|
||||
| 관리자 확장 | 주문 상세 KCP 거래 정보 표시, KCP 실행 환경(CLI/SOAP) 점검 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 동작 방식
|
||||
|
||||
<!-- @intent START -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[체크아웃 주문 생성] -->|PC| B["payplus_web.jsp 결제창 iframe 로드"]
|
||||
A -->|모바일| C["/mobile/approval-key 호출 → SOAP 승인키 발급"]
|
||||
B --> D["/payment/callback (enc_data·enc_info)"]
|
||||
C --> E["모바일 결제창 → /payment/callback"]
|
||||
D --> F[서버가 KCP CLI 실행해 승인 확인]
|
||||
E --> F
|
||||
F --> G[주문 결제 완료 처리]
|
||||
G --> H[성공 URL 리다이렉트]
|
||||
```
|
||||
|
||||
PC 결제 승인은 `NhnKcpApiService`가 OS 를 판별해 `pp_cli`/`pp_cli_x64`/`pp_cli_exe.exe` 중
|
||||
하나를 `exec()`로 실행하는 방식입니다. 모든 CLI 인자는 `assertSafeCliValue()`로 위험
|
||||
문자·제어문자를 사전 거부한 뒤 `escapeshellarg()`로 quoting 합니다 — KCP 응답값을 검증 없이
|
||||
셸 명령에 넣으면 명령 삽입(command injection) 통로가 됩니다. `PreventsReplayCallback`
|
||||
트레이트가 콜백 진입 시점에 동일 거래번호가 이미 결제완료 상태인지 확인해 중복 처리를
|
||||
막습니다.
|
||||
|
||||
가상계좌는 결제창에서 발급되면 주문이 입금대기 상태로 유지되다가, KCP 가 입금통보 URL로
|
||||
결과를 POST 하면 금액 검증 후 결제 완료 처리됩니다. 에스크로는 KCP 공통통보의 `tx_cd`/
|
||||
`cl_status` 조합을 해석해 구매확인/구매취소/구매취소확인/배송시작 4가지 훅으로 분기
|
||||
발화합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 요구 사항
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| G7 | `>= 7.0.0-beta.2` |
|
||||
| 의존 모듈 | `sirsoft-ecommerce >= 1.0.0-beta.5` |
|
||||
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| G7 코어 | `>=7.0.10` |
|
||||
| PHP | `^8.2` |
|
||||
| 의존 모듈 | `sirsoft-ecommerce` `>=1.1.0` |
|
||||
<!-- @generated:requirements END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
| 항목 | 필요한 것 |
|
||||
|---|---|
|
||||
| PC 결제 | PHP `exec()` 사용 가능, KCP CLI 바이너리, `pub.key` |
|
||||
| 모바일 결제 | PHP SOAP 확장, KCP WSDL 파일 |
|
||||
| 운영 환경 | HTTPS 도메인, 올바른 `APP_URL`, KCP 가맹점 계약 정보 |
|
||||
@@ -47,61 +113,70 @@ chmod 755 plugins/sirsoft-pay_nhnkcp/bin/pp_cli
|
||||
chmod 755 plugins/sirsoft-pay_nhnkcp/bin/pp_cli_x64
|
||||
```
|
||||
|
||||
관리자 설정 화면의 시스템 점검 API가 실행 권한을 자동 복구할 수 있지만, 서버 권한 정책에 따라 직접 조치가 필요할 수 있습니다.
|
||||
관리자 설정 화면의 시스템 점검 API 와 결제 hot path 의 자가 복구(`ensureCliExecutable()`)가
|
||||
실행 권한을 자동 복구할 수 있지만, 서버 권한 정책에 따라 직접 조치가 필요할 수 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 설치
|
||||
|
||||
플러그인을 G7 프로젝트의 플러그인 디렉토리에 배치합니다.
|
||||
|
||||
```text
|
||||
plugins/sirsoft-pay_nhnkcp
|
||||
```
|
||||
|
||||
프론트엔드 에셋을 수정한 경우 플러그인 디렉토리에서 빌드합니다.
|
||||
|
||||
<!-- @generated:install START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
# 번들 설치 (코어에 동봉된 소스에서 설치)
|
||||
php artisan plugin:install sirsoft-pay_nhnkcp
|
||||
|
||||
# 활성화
|
||||
php artisan plugin:activate sirsoft-pay_nhnkcp
|
||||
|
||||
# 업데이트 (번들 소스 기준 강제 반영)
|
||||
php artisan plugin:update sirsoft-pay_nhnkcp --force
|
||||
```
|
||||
|
||||
그다음 G7 관리자에서 플러그인을 활성화하고, 이커머스 결제 설정에서 PG 제공자를 `NHN KCP`로 선택합니다.
|
||||
저장소: https://github.com/gnuboard/g7-plugin-sirsoft-pay_nhnkcp
|
||||
<!-- @generated:install END -->
|
||||
|
||||
설치·활성화 후 이커머스 결제 설정에서 PG 제공자를 "NHN KCP"로 선택해야 실제로 결제 흐름에
|
||||
연결됩니다 — 활성화만으로는 체크아웃 화면에 나타나지 않습니다.
|
||||
|
||||
## 관리자 설정
|
||||
|
||||
관리자 플러그인 설정 화면에서 KCP 계약 정보를 입력합니다.
|
||||
<!-- @generated:settings-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 의미 | 기본값 |
|
||||
|---|---|---|
|
||||
| `is_test_mode` | 테스트 모드 | `true` |
|
||||
| `test_site_cd` | 테스트 사이트 코드 (site_cd) | `T0000` |
|
||||
| `test_site_key` | 테스트 사이트 키 (site_key) | - |
|
||||
| `live_site_cd` | 라이브 사이트 코드 (site_cd) | - |
|
||||
| `live_site_key` | 라이브 사이트 키 (site_key) | - |
|
||||
| `redirect_success_url` | 결제 성공 리다이렉트 URL | `{shopBase}/orders/{orderId}/complete` |
|
||||
| `redirect_fail_url` | 결제 실패 리다이렉트 URL | `{shopBase}/checkout` |
|
||||
| `use_escrow` | 에스크로 결제 활성화 | `false` |
|
||||
| `escrow_test_site_cd` | 테스트 에스크로 사이트 코드 | - |
|
||||
| `vbank_expire_days` | 가상계좌 입금 만료(일) | `3` |
|
||||
| `easy_pay_allow_with_other_pg` | - | `false` |
|
||||
| `easy_pay_payco` | - | `false` |
|
||||
| `easy_pay_naverpay` | - | `false` |
|
||||
| `easy_pay_naverpay_point` | - | `false` |
|
||||
| `easy_pay_kakaopay` | - | `false` |
|
||||
| `easy_pay_applepay` | - | `false` |
|
||||
|
||||
| 설정 | 설명 |
|
||||
|------|------|
|
||||
| 테스트 모드 | 활성화 시 KCP 테스트 환경을 사용합니다. |
|
||||
| 테스트 사이트 코드 | 기본값은 `T0000`입니다. |
|
||||
| 테스트 사이트 키 | KCP 테스트 site key입니다. |
|
||||
| 라이브 사이트 코드 | 운영 site code입니다. `SR` prefix 없이 입력해도 플러그인이 자동 보정합니다. |
|
||||
| 라이브 사이트 키 | 운영 site key입니다. 외부에 노출하지 마세요. |
|
||||
| 결제 성공 URL | 기본값은 `/shop/orders/{orderId}/complete`입니다. |
|
||||
| 결제 실패 URL | 기본값은 `/shop/checkout`입니다. |
|
||||
| 가상계좌 입금 만료일 | 가상계좌 발급 후 입금 가능 기간입니다. |
|
||||
| 에스크로 결제 사용 | 활성화 시 KCP 에스크로 결제 파라미터를 함께 전달합니다. |
|
||||
| 에스크로 테스트 사이트 코드 | 테스트 에스크로 site code입니다. 기본 fallback은 `T0007`입니다. |
|
||||
| 간편결제 | KCP 계약이 완료된 간편결제만 활성화하세요. |
|
||||
| 타 PG와 사용가능함 | 다른 PG가 기본값이어도 KCP 간편결제 버튼을 체크아웃 화면에 표시합니다. |
|
||||
개발자용 상세(타입·검증·저장 위치)는 [설정 스키마](docs/settings.md#설정-스키마) 를 보세요.
|
||||
<!-- @generated:settings-summary END -->
|
||||
|
||||
PAYCO 테스트 결제는 내부 기본값으로 간편결제 테스트 site code `S6729`를 사용합니다.
|
||||
<!-- @intent START -->
|
||||
라이브 사이트 키는 외부에 노출하지 마세요. 배포 전 테스트 모드가 의도한 값인지 반드시
|
||||
확인하세요.
|
||||
|
||||
## 콜백 및 통보 URL
|
||||
|
||||
KCP 가맹점 관리자에 아래 URL을 등록합니다. 도메인은 실제 운영 도메인으로 바꿔 입력하세요.
|
||||
**콜백 및 통보 URL 등록** — KCP 가맹점 관리자에 아래 URL을 실제 운영 도메인으로 등록합니다.
|
||||
|
||||
| 용도 | URL |
|
||||
|------|-----|
|
||||
| 결제 결과 Return URL | `https://your-domain.com/plugins/sirsoft-pay_nhnkcp/payment/callback` |
|
||||
| 가상계좌 입금통보 URL | `https://your-domain.com/plugins/sirsoft-pay_nhnkcp/payment/vbank-notify` |
|
||||
| 에스크로 공통통보 URL | `https://your-domain.com/plugins/sirsoft-pay_nhnkcp/payment/escrow-common-notify` |
|
||||
|---|---|
|
||||
| 결제 결과 Return URL | `https://{도메인}/plugins/sirsoft-pay_nhnkcp/payment/callback` |
|
||||
| 가상계좌 입금통보 URL | `https://{도메인}/plugins/sirsoft-pay_nhnkcp/payment/vbank-notify` |
|
||||
| 에스크로 공통통보 URL | `https://{도메인}/plugins/sirsoft-pay_nhnkcp/payment/escrow-common-notify` |
|
||||
|
||||
결제 결과 Return URL은 브라우저가 POST하는 경로이므로 IP 제한을 적용하지 않습니다. 가상계좌 입금통보와 에스크로 공통통보는 KCP 서버가 직접 호출하므로 운영 모드에서 IP 화이트리스트를 적용합니다.
|
||||
|
||||
## IP 화이트리스트
|
||||
|
||||
운영 모드에서는 아래 IP에서 들어온 KCP 서버 통보만 허용합니다. 테스트 모드에서는 개발과 KCP testadmin 모의입금을 위해 IP 제한을 우회합니다.
|
||||
결제 결과 Return URL은 브라우저가 POST하는 경로이므로 IP 제한을 적용하지 않습니다. 가상계좌
|
||||
입금통보와 에스크로 공통통보는 KCP 서버가 직접 호출하므로 운영 모드에서 아래 IP
|
||||
화이트리스트를 적용합니다(테스트 모드에서는 개발·KCP testadmin 모의입금을 위해 우회).
|
||||
|
||||
| IP |
|
||||
|----|
|
||||
@@ -116,49 +191,22 @@ KCP 가맹점 관리자에 아래 URL을 등록합니다. 도메인은 실제
|
||||
| `210.122.72.173` |
|
||||
|
||||
운영 전 KCP 가맹점 관리자와 최신 연동 가이드의 통보 서버 IP를 다시 확인하세요.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 결제 흐름
|
||||
## 사용 방법
|
||||
|
||||
### PC 결제
|
||||
<!-- @intent START -->
|
||||
**결제 취소/부분취소**: 관리자가 주문 취소를 요청(`cancel_pg=true`)하면 코어가
|
||||
`sirsoft-ecommerce.payment.refund` 필터 훅을 발화하고, 이 플러그인의 `PaymentRefundListener`
|
||||
가 KCP 취소 API를 호출합니다(전액취소는 `isPartial=false`, 부분취소는 `isPartial=true` +
|
||||
원래 결제금액). 배송비가 포함된 주문은 전체취소 시 배송비도 함께 환불 레코드에 반영되고,
|
||||
쿠폰이 적용된 주문은 실결제금액(쿠폰 차감 후)이 PG `cancelAmt`로 전달됩니다. 부분취소로
|
||||
쿠폰 최소 주문금액 조건을 더 이상 충족하지 못하면 코어가 취소 자체를 거부(422)해 PG 호출이
|
||||
아예 발생하지 않습니다. KCP API 호출이 실패하면 주문 상태 변경이 롤백됩니다.
|
||||
|
||||
```text
|
||||
체크아웃 주문 생성
|
||||
→ 프론트엔드 핸들러가 payplus_web.jsp 결제창 실행
|
||||
→ KCP가 /payment/callback 으로 enc_data, enc_info POST
|
||||
→ 서버가 KCP CLI로 승인 확인
|
||||
→ 주문 결제 완료 처리
|
||||
→ 성공 URL로 리다이렉트
|
||||
```
|
||||
|
||||
### 모바일 결제
|
||||
|
||||
```text
|
||||
체크아웃 주문 생성
|
||||
→ /api/plugins/sirsoft-pay_nhnkcp/mobile/approval-key 호출
|
||||
→ 서버가 KCP SOAP approve 로 approval_key, pay_url 획득
|
||||
→ 브라우저가 pay_url 로 form POST
|
||||
→ KCP가 /payment/callback 으로 결과 POST
|
||||
→ 주문 결제 완료 처리
|
||||
→ 성공 URL로 리다이렉트
|
||||
```
|
||||
|
||||
### 가상계좌
|
||||
|
||||
```text
|
||||
결제창에서 가상계좌 발급
|
||||
→ 주문 결제 정보에 은행, 계좌번호, 예금주, 만료일 저장
|
||||
→ 주문은 입금대기 상태 유지
|
||||
→ KCP가 /payment/vbank-notify 로 입금통보 POST
|
||||
→ 입금 금액 검증 후 주문 결제 완료 처리
|
||||
```
|
||||
|
||||
테스트 모드에서는 마이페이지 주문 상세에 KCP testadmin 모의입금 폼이 표시될 수 있습니다.
|
||||
|
||||
### 에스크로
|
||||
|
||||
에스크로를 활성화하면 결제 요청에 `escw_used=Y`, `pay_mod=O`를 전달합니다. 에스크로 결제 완료 후 관리자 주문 상세에서 운송장번호와 택배사를 입력해 KCP 배송 등록을 호출할 수 있습니다.
|
||||
|
||||
KCP 공통통보는 아래 이벤트를 처리합니다.
|
||||
**에스크로 처리**: 에스크로를 활성화하면 결제 요청에 `escw_used=Y`, `pay_mod=O`를
|
||||
전달합니다. 에스크로 결제 완료 후 관리자 주문 상세에서 운송장번호와 택배사를 입력해 KCP
|
||||
배송 등록을 호출할 수 있습니다. KCP 공통통보는 아래 이벤트를 처리합니다.
|
||||
|
||||
| tx_cd | 조건 | 처리 |
|
||||
|-------|------|------|
|
||||
@@ -167,82 +215,64 @@ KCP 공통통보는 아래 이벤트를 처리합니다.
|
||||
| `TX02` | `cl_status=3` | 구매취소 확인 훅 실행 |
|
||||
| `TX03` | - | 배송시작 훅 실행 |
|
||||
|
||||
### 결제 취소 / 부분취소
|
||||
**가상계좌 모의입금**: 테스트 모드에서는 마이페이지 주문 상세에 KCP testadmin 모의입금
|
||||
폼이 표시될 수 있습니다.
|
||||
|
||||
```text
|
||||
관리자 주문 취소 요청 (cancel_pg=true)
|
||||
→ 코어가 sirsoft-ecommerce.payment.refund 필터 훅 발화
|
||||
→ PaymentRefundListener 가 KCP cancelPayment API 호출
|
||||
· 전액취소: isPartial=false
|
||||
· 부분취소: isPartial=true, totalAmt=원래 결제금액
|
||||
→ 코어가 환불 레코드 생성 + 쿠폰 / 마일리지 / 재고 복원
|
||||
→ CancelActivityLogListener 가 PG 응답 시각·취소 거래번호를 활동 로그에 기록
|
||||
```
|
||||
전체 API 목록(사용자/관리자)은 [docs/api/](docs/api/README.md) 를, 발행/구독 훅 목록은
|
||||
[docs/extension-points.md](docs/extension-points.md) 를 참고하세요.
|
||||
<!-- @intent END -->
|
||||
|
||||
배송비가 포함된 주문은 전체취소 시 배송비도 함께 환불 레코드에 반영되고, 쿠폰이 적용된 주문은 실결제금액(쿠폰 차감 후) 이 PG cancelAmt 로 전달됩니다. 부분취소 시 쿠폰 최소 주문금액 조건을 더 이상 충족하지 못하면 코어가 취소 자체를 거부 (422) 하여 PG 호출이 발생하지 않습니다. KCP API 호출이 실패하면 주문 상태 변경이 롤백됩니다.
|
||||
## 다른 확장과의 연동
|
||||
|
||||
## API
|
||||
<!-- @generated:integrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
### 사용자 API
|
||||
| 확장 | 유형 | 버전 제약 | 번들 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-ecommerce` | 모듈 | `>=1.1.0` | ✅ |
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| `GET` | `/api/plugins/sirsoft-pay_nhnkcp/user/orders/{orderNumber}/receipt` | KCP 영수증, 현금영수증 URL 조회 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_nhnkcp/user/orders/{orderNumber}/vbank-mock-deposit-info` | 테스트 모드 가상계좌 모의입금 정보 조회 |
|
||||
| `POST` | `/api/plugins/sirsoft-pay_nhnkcp/mobile/approval-key` | 모바일 결제 승인키 발급 |
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
### 관리자 API
|
||||
없음.
|
||||
<!-- @generated:integrations END -->
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| `GET` | `/api/plugins/sirsoft-pay_nhnkcp/admin/vbank-notify-url` | 가상계좌/에스크로 통보 URL 조회 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_nhnkcp/admin/orders/test-mode-map` | 주문목록 테스트 모드 배지용 맵 조회 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_nhnkcp/admin/orders/{orderNumber}/transaction-status` | 저장된 KCP 거래 정보 조회 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_nhnkcp/admin/orders/{orderNumber}/escrow-delivery` | 에스크로 배송 등록 폼 데이터 조회 |
|
||||
| `POST` | `/api/plugins/sirsoft-pay_nhnkcp/admin/orders/{orderNumber}/escrow-delivery` | KCP 에스크로 배송 등록 |
|
||||
| `GET` | `/api/plugins/sirsoft-pay_nhnkcp/admin/health` | KCP 실행 환경 점검 |
|
||||
<!-- @intent START -->
|
||||
`RegisterPgProviderListener`가 이 플러그인을 이커머스의 PG 제공자 레지스트리에,
|
||||
`RegisterEasyPayMethodsListener`가 간편결제 결제수단 레지스트리에 각각 등록합니다 — PG
|
||||
결제사 선택과 간편결제 노출은 서로 독립적이라, 다른 PG가 기본값이어도 KCP 간편결제 버튼을
|
||||
체크아웃 화면에 노출하는 조합이 가능합니다(`easy_pay_allow_with_other_pg`).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 훅
|
||||
## 문서
|
||||
|
||||
다른 모듈이나 플러그인에서 아래 훅에 연결해 결제 흐름을 확장할 수 있습니다.
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
|
||||
| 훅 | 타입 | 시점 |
|
||||
|----|------|------|
|
||||
| `sirsoft-pay_nhnkcp.payment.before_confirm` | action | KCP CLI 승인 확인 전 |
|
||||
| `sirsoft-pay_nhnkcp.payment.after_confirm` | action | KCP CLI 승인 확인 후 |
|
||||
| `sirsoft-pay_nhnkcp.payment.before_cancel` | action | KCP 취소 API 호출 전 |
|
||||
| `sirsoft-pay_nhnkcp.payment.after_cancel` | action | KCP 취소 API 호출 후 |
|
||||
| `sirsoft-pay_nhnkcp.escrow.purchase_confirmed` | action | 에스크로 구매확인 통보 수신 |
|
||||
| `sirsoft-pay_nhnkcp.escrow.purchase_cancelled` | action | 에스크로 구매취소 통보 수신 |
|
||||
| `sirsoft-pay_nhnkcp.escrow.denial_confirmed` | action | 에스크로 구매취소 확인 통보 수신 |
|
||||
| `sirsoft-pay_nhnkcp.escrow.delivery_started` | action | 에스크로 배송시작 통보 수신 |
|
||||
## 트러블슈팅
|
||||
|
||||
## 보안 및 운영 참고
|
||||
<!-- @intent START -->
|
||||
| 증상 | 원인 | 조치 |
|
||||
|---|---|---|
|
||||
| 결제 승인 시 res_cd=9502 오류 | `plugin:update` 가 CLI 바이너리 실행 권한을 0664 로 되돌림 | 관리자 설정 화면의 시스템 점검을 실행하거나 `chmod 755` 로 직접 복구 |
|
||||
| 가상계좌 입금통보가 반영되지 않음 | 운영 환경 IP 화이트리스트에 KCP 통보 서버 IP가 없음 | 최신 연동 가이드의 통보 서버 IP로 화이트리스트를 갱신 |
|
||||
| 결제 요청이 CLI 인자 오류로 거부됨 | 주문번호·인코딩 데이터 등에 위험 문자/제어문자 포함 | `assertSafeCliValue()`가 의도적으로 거부한 것 — 원인 값을 정제하지 말고 왜 그런 값이 만들어졌는지 상위 데이터를 확인 |
|
||||
| 모바일 결제 승인키 발급 실패 | PHP SOAP 확장 미설치 또는 WSDL 파일 누락 | `php -m`으로 soap 확장 확인, `bin/*.wsdl` 존재 확인 |
|
||||
| 결제 성공했는데 간편결제 버튼 클릭 시 오류 | KCP 계약이 없는 결제수단/간편결제를 활성화 | 계약이 완료된 결제수단만 관리자 설정에서 활성화 |
|
||||
<!-- @intent END -->
|
||||
|
||||
- 운영 도메인의 `APP_URL`을 HTTPS 절대 URL로 정확히 설정하세요.
|
||||
- 운영 site key는 외부에 노출하지 마세요.
|
||||
- 운영 모드에서는 KCP 서버 통보 IP 화이트리스트가 적용됩니다.
|
||||
- 결제 승인 후 서버 후속 처리에 실패하면 PG 잔존 승인을 자동 취소합니다.
|
||||
- 동일 거래번호 콜백은 중복 처리하지 않도록 방어합니다.
|
||||
- KCP CLI 호출 인자는 위험 문자와 제어문자를 사전에 거부합니다.
|
||||
- 가상계좌와 에스크로 통보 URL은 KCP 가맹점 관리자에 반드시 등록해야 합니다.
|
||||
- KCP 계약이 없는 결제수단이나 간편결제를 활성화하면 KCP 오류가 발생할 수 있습니다.
|
||||
## 변경 이력
|
||||
|
||||
## 테스트
|
||||
|
||||
플러그인을 G7 프로젝트에 배치한 뒤 G7 루트에서 PHP 테스트를 실행합니다.
|
||||
|
||||
```bash
|
||||
php artisan test plugins/sirsoft-pay_nhnkcp/tests
|
||||
```
|
||||
|
||||
프론트엔드 테스트와 빌드는 플러그인 디렉토리에서 실행합니다.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run test:run
|
||||
npm run build
|
||||
```
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 라이선스
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# NHN KCP 개발자 문서
|
||||
|
||||
> plugins/_bundled/sirsoft-pay_nhnkcp · 플러그인
|
||||
|
||||
<!-- @generated:stats START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**훅 수**: 8 · **구독 훅 수**: 9 · **라우트 수**: 16 · **모델 수**: 0 · **테이블 수**: 0 · **마이그레이션 수**: 0 · **레이아웃 수**: 1 · **핸들러 수**: 3
|
||||
<!-- @generated:stats END -->
|
||||
|
||||
## 문서 목차
|
||||
|
||||
<!-- @generated:doc-toc START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | 설계 의도·계층 지도·디렉토리 맵 |
|
||||
| [extension-points.md](extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 |
|
||||
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
|
||||
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
|
||||
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
|
||||
| [api/](api/README.md) | API 레퍼런스 |
|
||||
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
|
||||
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
|
||||
<!-- @generated:doc-toc END -->
|
||||
@@ -0,0 +1,63 @@
|
||||
# NHN KCP — 아키텍처
|
||||
|
||||
> 설계 의도와 계층 구조 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설계 의도
|
||||
|
||||
<!-- @intent START -->
|
||||
NHN KCP 표준결제(Standard Pay)를 `sirsoft-ecommerce` 에 연결하는 어댑터입니다. 다른 PG
|
||||
플러그인(`sirsoft-pay_kginicis`, `sirsoft-tosspayments` 등)이 전부 HTTP API 로 승인을
|
||||
받는 것과 달리, 이 플러그인의 PC 결제 승인은 **서버에서 KCP CLI 바이너리를 실행**하는
|
||||
방식입니다 — KCP 가 표준결제 승인 로직을 컴파일된 실행파일로만 배포하기 때문입니다. 이
|
||||
차이가 데이터 모델(§data-model.md — Repository 조차 없는 이유), 확장점(CLI 실행 권한
|
||||
점검 API), 금지 패턴(CLI 인자 injection 방어) 전체에 스며 있습니다.
|
||||
|
||||
이 플러그인도 결제 상태 자체는 소유하지 않습니다 — 주문·결제 테이블은 `sirsoft-ecommerce`
|
||||
소유이고, 이 플러그인은 "그 상태를 KCP CLI/SOAP API 와 어떻게 주고받는가"만 책임집니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 계층 지도
|
||||
|
||||
<!-- @intent START -->
|
||||
```text
|
||||
Controller (PaymentCallbackController / EscrowCommonNotifyController / MobileApprovalController)
|
||||
→ NhnKcpApiService (CLI 실행 · SOAP 호출 · 인자 사전검증)
|
||||
→ sirsoft-ecommerce 의 Order/OrderPayment 모델 (직접 참조 — 이 플러그인 소유 모델 없음)
|
||||
|
||||
Listener (RegisterPgProviderListener 등)
|
||||
→ sirsoft-ecommerce 의 필터 훅에 등록 (컴파일 타임 결합 없음)
|
||||
```
|
||||
|
||||
이 플러그인에는 FormRequest 계층이 얕습니다 — 결제 승인·통보 콜백은 사용자가 채운 폼이
|
||||
아니라 KCP 가 보내는 고정 스키마이므로, 검증의 대부분은 `NhnKcpApiService`의
|
||||
`assertSafeCliValue()`(CLI 인자 안전성)와 `PreventsReplayCallback`(중복 콜백 방지)이
|
||||
담당합니다. 일반 CRUD 플러그인의 "Controller → FormRequest → Service → Repository → Model"
|
||||
5단 계층과 다른 이유가 여기 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 디렉토리
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-pay_nhnkcp --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-pay_nhnkcp --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-pay_nhnkcp --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-pay_nhnkcp --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-pay_nhnkcp --force` |
|
||||
| `bin/` | 확장이 실행하는 외부 바이너리·인증서 | 교체 시 OS별 파일과 권한을 함께 확인 (비면 해당 기능 정지) |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
@@ -0,0 +1,69 @@
|
||||
# NHN KCP — 데이터 모델
|
||||
|
||||
> 모델·소유 테이블·마이그레이션·Enum · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 모델
|
||||
|
||||
<!-- @generated:models START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_소유 모델이 없습니다._
|
||||
<!-- @generated:models END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 상태는 이 플러그인이 아니라 `sirsoft-ecommerce`의 `Order`/`OrderPayment` 모델이
|
||||
소유합니다(§AGENTS.md "설계 원칙"). 이 플러그인은 그 모델을 직접 참조해 읽고 쓸 뿐, 자기
|
||||
Repository 조차 두지 않았습니다(§Repository) — `sirsoft-pay_kginicis`가 CBT(일본) 정산용
|
||||
Repository 2개를 갖는 것과 달리, 이 플러그인은 일본/CBT 결제를 아예 구현하지 않으므로
|
||||
그 계층이 존재할 이유가 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 소유 테이블
|
||||
|
||||
<!-- @generated:tables START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_소유 테이블이 없습니다._
|
||||
<!-- @generated:tables END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
가상계좌 발급 정보(은행·계좌번호·예금주·만료일)와 KCP 거래 정보(`tno` 등)는 이커머스
|
||||
`OrderPayment` 테이블의 기존 컬럼/메타에 저장됩니다 — PG 마다 별도 결제상세 테이블을 두면
|
||||
관리자 주문 상세가 PG 종류에 따라 다른 테이블을 조인해야 해 화면 로직이 PG 개수만큼
|
||||
분기합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 마이그레이션
|
||||
|
||||
<!-- @generated:migrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_마이그레이션이 없습니다._
|
||||
<!-- @generated:migrations END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
소유 테이블이 없으므로(§소유 테이블) 스키마 변경 자체가 발생하지 않습니다. 이 플러그인의
|
||||
설정 스키마 변경(§settings.md)은 `config/settings/defaults.json` 갱신만으로 끝나며 DB
|
||||
마이그레이션 대상이 아닙니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Enum
|
||||
|
||||
<!-- @generated:enums START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_Enum 이 없습니다._
|
||||
<!-- @generated:enums END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
KCP 공통통보의 `tx_cd`/`cl_status` 값은 Enum 대신 `EscrowCommonNotifyController`(§extension-points.md
|
||||
"핵심 흐름"의 매핑표)의 조건 분기로 직접 처리합니다 — 이 값들은 이 플러그인 코드 어디에도
|
||||
재사용되지 않는 KCP 고유 프로토콜 상수라, Enum 으로 승격해도 얻는 타입 안전성 대비 간접
|
||||
계층만 늘어납니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Repository
|
||||
|
||||
<!-- @generated:repositories START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_Repository 가 없습니다._
|
||||
<!-- @generated:repositories END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
이 플러그인이 이커머스 `Order`/`OrderPayment`를 읽고 쓰는 지점(컨트롤러·리스너·`Concerns`
|
||||
트레이트)은 모두 이커머스가 이미 노출한 Eloquent 모델을 직접 참조합니다 — 자기 소유
|
||||
테이블이 없는 상태에서 남의 모델을 감싸는 Repository 를 새로 만드는 것은 위임만 하는
|
||||
빈 계층입니다. `sirsoft-pay_kginicis`의 CBT Repository 2개는 이 플러그인에는 없는
|
||||
일본 결제 전용 정산 데이터(자체 소유 테이블)를 다루기 위한 것이라 대칭이 아닙니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,148 @@
|
||||
# NHN KCP — 확장점
|
||||
|
||||
> 발행/구독 훅·미들웨어·채널·스케줄 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 발행 훅
|
||||
|
||||
<!-- @generated:hooks-published START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
발행 훅 8종 / 호출 지점 8곳. 이 중 4종은 `getHooks()` 선언에 없어 소스에서 자동 감지한 것입니다 — 선언에 추가하면 유형과 설명이 함께 실립니다.
|
||||
|
||||
| 훅 이름 | 유형 | 설명 | 발행 위치 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-pay_nhnkcp.escrow.delivery_started` | action | — | `src/Controllers/EscrowCommonNotifyController.php:101` |
|
||||
| `sirsoft-pay_nhnkcp.escrow.denial_confirmed` | action | — | `src/Controllers/EscrowCommonNotifyController.php:98` |
|
||||
| `sirsoft-pay_nhnkcp.escrow.purchase_cancelled` | action | — | `src/Controllers/EscrowCommonNotifyController.php:97` |
|
||||
| `sirsoft-pay_nhnkcp.escrow.purchase_confirmed` | action | — | `src/Controllers/EscrowCommonNotifyController.php:96` |
|
||||
| `sirsoft-pay_nhnkcp.payment.after_cancel` | action | KCP 결제 취소 완료 후 | `src/Services/NhnKcpApiService.php:250` |
|
||||
| `sirsoft-pay_nhnkcp.payment.after_confirm` | action | KCP 결제 승인 확인 완료 후 | `src/Controllers/PaymentCallbackController.php:279` |
|
||||
| `sirsoft-pay_nhnkcp.payment.before_cancel` | action | KCP 결제 취소 API 호출 전 (본인인증 등 확장 지점) | `src/Services/NhnKcpApiService.php:229` |
|
||||
| `sirsoft-pay_nhnkcp.payment.before_confirm` | action | KCP 결제 승인 확인 전 | `src/Controllers/PaymentCallbackController.php:274` |
|
||||
<!-- @generated:hooks-published END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`escrow.*` 4종에 `유형`/`설명`이 비어 있는 것은 실수가 아니라 선언 누락입니다 — 소스에서
|
||||
자동 감지된 훅이라 `getHooks()`에 등록하면 이름 그대로도 의미가 분명해 설명을 생략했습니다.
|
||||
`before_confirm`/`before_cancel`은 KCP API 호출 **전** 개입 지점이라 여기서 예외를 던지면
|
||||
실제 KCP 호출 자체가 일어나지 않습니다(예: 고액 결제에 추가 인증을 요구하고 싶은 확장이
|
||||
`before_cancel`에서 조건 미충족 시 예외). `after_*`는 응답을 받은 뒤 부가효과(로그, 알림)를
|
||||
붙이는 자리입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 구독 훅
|
||||
|
||||
<!-- @generated:hooks-subscribed START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 훅 이름 | 유형 | 리스너 | 메서드 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| `core.layout_extension.after_apply` | filter | `AdjustEcommercePaymentMethodsLayoutListener` | `adjustPaymentMethodsLayout` | 30 |
|
||||
| `core.layout_extension.after_apply` | filter | `EnsureAdminOrderDetailPaymentQueryLayoutListener` | `ensurePaymentQueryLayout` | 66 |
|
||||
| `core.layout_extension.after_apply` | filter | `EnsureAdminOrderListTestBadgeLayoutListener` | `ensureTestBadgeLayout` | 60 |
|
||||
| `core.plugins.updated` | action | `RestoreLayoutExtensionsAfterUpdateListener` | `restoreCurrentExtensionsAfterUpdate` | 20 |
|
||||
| `sirsoft-ecommerce.payment.get_client_config` | filter | `RegisterPgProviderListener` | `getClientConfig` | 10 |
|
||||
| `sirsoft-ecommerce.payment.refund` | filter | `CancelActivityLogListener` | `logCancelConfirmed` | 20 |
|
||||
| `sirsoft-ecommerce.payment.refund` | filter | `PaymentRefundListener` | `processRefund` | 10 |
|
||||
| `sirsoft-ecommerce.payment.registered_pg_providers` | filter | `RegisterPgProviderListener` | `registerProvider` | 10 |
|
||||
| `sirsoft-ecommerce.settings.filter_available_payment_methods` | filter | `RegisterEasyPayMethodsListener` | `injectEasyPayMethods` | 30 |
|
||||
<!-- @generated:hooks-subscribed END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`RegisterPgProviderListener` 가 우선순위 10 으로 두 훅(`get_client_config`/
|
||||
`registered_pg_providers`)을 모두 구독하는 이유는 "이 PG 가 존재한다는 사실"과 "체크아웃
|
||||
화면이 필요로 하는 클라이언트 설정값"이 같은 리스너의 책임이기 때문입니다 — 등록과 설정
|
||||
노출이 다른 리스너로 갈라지면 한쪽만 갱신되는 사각이 생깁니다. `PaymentRefundListener`(10)
|
||||
가 `CancelActivityLogListener`(20)보다 먼저 실행되도록 우선순위를 명시한 것은 실제 취소가
|
||||
성공한 뒤에야 활동 로그를 남기기 위함입니다 — 순서가 뒤바뀌면 "로그는 있는데 취소는 실패"가
|
||||
생깁니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 훅 리스너
|
||||
|
||||
<!-- @generated:listeners START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 리스너 | 구독 훅 | 등록 방식 | HookListenerInterface | 파일 |
|
||||
|---|---|---|---|---|
|
||||
| `AdjustEcommercePaymentMethodsLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/AdjustEcommercePaymentMethodsLayoutListener.php` |
|
||||
| `CancelActivityLogListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/CancelActivityLogListener.php` |
|
||||
| `EnsureAdminOrderDetailPaymentQueryLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/EnsureAdminOrderDetailPaymentQueryLayoutListener.php` |
|
||||
| `EnsureAdminOrderListTestBadgeLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/EnsureAdminOrderListTestBadgeLayoutListener.php` |
|
||||
| `PaymentRefundListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/PaymentRefundListener.php` |
|
||||
| `RegisterEasyPayMethodsListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/RegisterEasyPayMethodsListener.php` |
|
||||
| `RegisterPgProviderListener` | 2개 | 명시 등록 | ✅ | `src/Listeners/RegisterPgProviderListener.php` |
|
||||
| `RestoreLayoutExtensionsAfterUpdateListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/RestoreLayoutExtensionsAfterUpdateListener.php` |
|
||||
<!-- @generated:listeners END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`RestoreLayoutExtensionsAfterUpdateListener`가 존재하는 이유는 `plugin:update`가 레이아웃
|
||||
확장 조각(§레이아웃 확장)의 활성/비활성 상태를 초기화할 수 있어서입니다 — 운영자가 특정
|
||||
화면(예: 테스트배지)을 꺼둔 상태로 플러그인을 업데이트해도 그 선택이 사라지지 않도록
|
||||
업데이트 직후 복원합니다. 8개 리스너 전부가 `HookListenerInterface`를 구현하는 것은
|
||||
auto-discovery 대상이라는 뜻이 아니라 이 저장소의 전 리스너 공통 계약입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 레이아웃 확장
|
||||
|
||||
<!-- @generated:layout-extensions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 대상 | 설명 |
|
||||
|---|---|
|
||||
| `resources/extensions/admin_order_list_test_badge.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/admin_order_payment_query.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/checkout_easy_pay.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/user_order_complete_receipt.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/user_order_show.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
<!-- @generated:layout-extensions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
5개 조각은 각각 독립적인 화면 관심사입니다 — 관리자 주문 목록의 테스트배지, 관리자 주문
|
||||
상세의 거래조회 UI, 체크아웃의 간편결제 버튼, 주문완료/마이페이지의 영수증 버튼이 서로
|
||||
다른 화면·다른 컴포넌트 트리에 주입되므로 하나의 조각으로 합치지 않았습니다. 새 KCP 기능이
|
||||
필요로 하는 화면이 이 5개 중 하나에 해당하면 새 조각을 만들지 말고 기존 조각을 확장합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 미들웨어
|
||||
|
||||
<!-- @generated:middleware START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 미들웨어 | 부착 대상(targets) | 우선순위 |
|
||||
|---|---|---|
|
||||
| `RestrictKcpIp` | `web.plugins.sirsoft-pay_nhnkcp.payment.vbank-notify`, `web.plugins.sirsoft-pay_nhnkcp.payment.escrow-common-notify` | - |
|
||||
<!-- @generated:middleware END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 결과 Return URL(`/payment/callback`)에는 이 미들웨어가 붙지 않습니다 — 그 경로는
|
||||
브라우저가 POST 하는 경로라 발신 IP 가 사용자마다 다르기 때문입니다. IP 화이트리스트가
|
||||
의미 있는 것은 KCP 서버가 직접 호출하는 두 통보 경로(가상계좌 입금통보·에스크로 공통통보)
|
||||
뿐입니다. 테스트 모드에서는 개발 편의와 KCP testadmin 모의입금을 위해 이 제한을 우회합니다
|
||||
— 운영 모드로 전환할 때 이 우회가 함께 꺼지는지 확인해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 브로드캐스트 채널
|
||||
|
||||
<!-- @generated:channels START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 브로드캐스트 채널이 없습니다._
|
||||
<!-- @generated:channels END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 승인·통보는 전부 동기 HTTP 요청/응답 안에서 끝나는 흐름이라 실시간 브로드캐스트가
|
||||
필요한 지점이 없습니다 — 가상계좌 입금통보조차 KCP 서버의 POST 요청 하나로 완결됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 스케줄
|
||||
|
||||
<!-- @generated:schedules START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 스케줄이 없습니다._
|
||||
<!-- @generated:schedules END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
가상계좌 만료 처리(§settings.md `vbank_expire_days`)는 이 플러그인이 크론으로 직접 만료
|
||||
스캔을 하지 않고, 만료 이후 도착하는 KCP 입금통보를 거부하는 방식으로 처리됩니다 — 별도
|
||||
스케줄 작업이 필요 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 알림 정의
|
||||
|
||||
<!-- @generated:notifications START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 알림 정의가 없습니다._
|
||||
<!-- @generated:notifications END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 완료/실패 알림은 이커머스 모듈이 주문 상태 변화를 기준으로 발송하는 공용 알림에 이미
|
||||
포함됩니다 — PG 마다 별도 알림 정의를 만들면 같은 이벤트(결제완료)에 대해 PG 수만큼 중복
|
||||
알림 정의가 생깁니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,85 @@
|
||||
# NHN KCP — 프론트엔드
|
||||
|
||||
> 레이아웃·액션 핸들러·전역 진입점·에셋 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 레이아웃
|
||||
|
||||
<!-- @generated:layouts START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
레이아웃 1개 (루트: `resources/layouts`).
|
||||
|
||||
| 그룹 | 개수 |
|
||||
|---|---|
|
||||
| `admin` | 1개 |
|
||||
|
||||
| 레이아웃 | 그룹 | 종류 | extends |
|
||||
|---|---|---|---|
|
||||
| `plugin_settings` | `admin` | 화면 | `_admin_base` |
|
||||
<!-- @generated:layouts END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`sirsoft-pay_kginicis`와 마찬가지로 이 플러그인이 소유한 화면 레이아웃은 관리자 설정 화면
|
||||
하나뿐입니다 — 체크아웃·주문상세·마이페이지의 결제 UI는 이 플러그인 소유가 아니라
|
||||
§레이아웃 확장(다른 확장/템플릿 레이아웃에 주입되는 조각)으로 존재합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 액션 핸들러
|
||||
|
||||
<!-- @generated:handlers START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
핸들러 3개 (정의: `resources/js/handlers/index.ts`).
|
||||
|
||||
| 핸들러 | 레이아웃에서 부르는 이름 |
|
||||
|---|---|
|
||||
| `requestPayment` | `sirsoft-pay_nhnkcp.requestPayment` |
|
||||
| `setPaymentMethod` | `sirsoft-pay_nhnkcp.setPaymentMethod` |
|
||||
| `copyToClipboard` | `sirsoft-pay_nhnkcp.copyToClipboard` |
|
||||
<!-- @generated:handlers END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`sirsoft-pay_kginicis`가 핸들러 1개(`requestPayment`)로 끝나는 것과 달리 이 플러그인은
|
||||
3개입니다. `setPaymentMethod`가 별도로 필요한 이유는 KCP 간편결제 버튼(PAYCO/네이버페이/
|
||||
카카오페이/Apple Pay)이 레이아웃 컴포넌트가 아니라 KCP 가 제공하는 DOM 을 그대로 쓰기
|
||||
때문입니다 — React 상태로 선택 하이라이트를 그리는 대신 DOM 을 직접 조작해 선택된 버튼에
|
||||
테두리를 입힙니다(`updateEasyPayButtonStyles`). Apple Pay 는 iOS 모바일이 아니면 여기서
|
||||
바로 오류 모달을 띄우고 요청 자체를 막습니다 — KCP 서버까지 보냈다가 거부당하면 사용자가
|
||||
결제 실패 이유를 알 수 없기 때문입니다. `copyToClipboard`는 가상계좌 계좌번호 복사
|
||||
버튼처럼 결제와 무관한 범용 유틸리티라 KCP 고유 로직이 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 전역 진입점
|
||||
|
||||
<!-- @generated:frontend-entry START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 엔트리 파일 | `resources/js/index.ts` |
|
||||
| 전역 객체 | `window.__SirsoftNhnkcp` |
|
||||
| 재등록 진입점 | `initPlugin()` |
|
||||
|
||||
로케일 전환 시 코어가 이 진입점을 호출해 핸들러를 다시 등록합니다. 진입점은 핸들러 재등록만 수행하고 1회성 부팅 작업을 포함하지 않습니다.
|
||||
<!-- @generated:frontend-entry END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`window.__SirsoftNhnkcp`로 노출되는 이유는 코어가 로케일 전환 시 이 이름으로 재등록
|
||||
진입점을 찾기 때문입니다(§CLAUDE.md "재등록 진입점"). KCP 결제창 스크립트 자체는 이
|
||||
진입점이 미리 로드하지 않습니다 — 모든 방문자가 결제 페이지에 오는 것은 아니므로 전역
|
||||
부팅에서 미리 불러올 필요가 없습니다(`requestPayment` 핸들러가 실제 결제 시도 시점에만
|
||||
동적으로 로드).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 에셋
|
||||
|
||||
<!-- @generated:assets START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 구분 |
|
||||
|---|---|
|
||||
| `dist/js/plugin.iife.js` | 빌드 산출물 (커밋 대상) |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 (manifest) |
|
||||
|
||||
로딩 설정: `{"strategy":"global","priority":100,"dependencies":[]}`
|
||||
<!-- @generated:assets END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
KCP 가 제공하는 `payplus_web.jsp` SDK 는 이 목록에 없습니다 — `requestPayment` 핸들러가
|
||||
결제 시도 시점에 iframe 안으로 동기 로드하는 제3자 자산이라, 이 플러그인이 빌드 시
|
||||
번들링하는 `dist/` 산출물과는 다른 층입니다. CSS 산출물이 없는 것은 결제창 자체는 KCP 가
|
||||
그리고, 이 플러그인은 간편결제 버튼·복사 버튼 같은 최소한의 UI만 코어 컴포넌트로
|
||||
구성하기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,101 @@
|
||||
# NHN KCP — 설정·권한·라우트
|
||||
|
||||
> 설정 스키마·권한·메뉴·라우트·의존 관계 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설정 스키마
|
||||
|
||||
<!-- @generated:settings-schema START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 타입 | 기본값 | 설명 |
|
||||
|---|---|---|---|
|
||||
| `is_test_mode` | `boolean` | `true` | 테스트 모드 |
|
||||
| `test_site_cd` | `string` | `T0000` | 테스트 사이트 코드 (site_cd) |
|
||||
| `test_site_key` | `string` | - | 테스트 사이트 키 (site_key) |
|
||||
| `live_site_cd` | `string` | - | 라이브 사이트 코드 (site_cd) |
|
||||
| `live_site_key` | `string` | - | 라이브 사이트 키 (site_key) |
|
||||
| `redirect_success_url` | `string` | `{shopBase}/orders/{orderId}/complete` | 결제 성공 리다이렉트 URL |
|
||||
| `redirect_fail_url` | `string` | `{shopBase}/checkout` | 결제 실패 리다이렉트 URL |
|
||||
| `use_escrow` | `boolean` | `false` | 에스크로 결제 활성화 |
|
||||
| `escrow_test_site_cd` | `string` | - | 테스트 에스크로 사이트 코드 |
|
||||
| `vbank_expire_days` | `integer` | `3` | 가상계좌 입금 만료(일) |
|
||||
| `easy_pay_allow_with_other_pg` | `boolean` | `false` | - |
|
||||
| `easy_pay_payco` | `boolean` | `false` | - |
|
||||
| `easy_pay_naverpay` | `boolean` | `false` | - |
|
||||
| `easy_pay_naverpay_point` | `boolean` | `false` | - |
|
||||
| `easy_pay_kakaopay` | `boolean` | `false` | - |
|
||||
| `easy_pay_applepay` | `boolean` | `false` | - |
|
||||
|
||||
기본값 파일: `config/settings/defaults.json` · 설정 화면 레이아웃: `resources/layouts/admin/plugin_settings.json`
|
||||
<!-- @generated:settings-schema END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`test_*`/`live_*` 쌍 구조는 `sirsoft-pay_kginicis`와 동일한 이유입니다 — 테스트 모드와 운영
|
||||
모드가 완전히 다른 자격증명 집합을 쓰므로 `is_test_mode`를 켜고 꺼도 서로의 값을 덮어쓰지
|
||||
않습니다. kginicis 와 달리 `japan_*` 설정군이 전혀 없는 것은 이 플러그인이 KCP 의 일본/CBT
|
||||
결제 상품을 구현하지 않기 때문입니다(§data-model.md, §architecture.md) — 이 플러그인에
|
||||
일본 결제를 요구하는 요청이 오면 새 설정 키를 추가하는 대신 별도 플러그인 여부를 먼저
|
||||
검토해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 권한
|
||||
|
||||
<!-- @generated:permissions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_선언된 권한이 없습니다._
|
||||
<!-- @generated:permissions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 설정 접근 권한은 이커머스의 관리자 권한 체계 안에서 다뤄집니다 — PG 마다 별도 권한을
|
||||
선언하면 PG 를 여러 개 설치했을 때 "결제 설정을 볼 수 있는 사람"이라는 하나의 개념이
|
||||
플러그인 수만큼 중복 정의됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 메뉴
|
||||
|
||||
<!-- @generated:menus START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 메뉴가 없습니다._
|
||||
<!-- @generated:menus END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
설정 화면(`plugin_settings.json`)은 코어의 "플러그인 관리 > 설정" 공통 진입점을 통해
|
||||
접근합니다 — PG 플러그인마다 전용 사이드바 메뉴를 만들면 PG 를 여러 개 설치했을 때 메뉴가
|
||||
난립합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 라우트
|
||||
|
||||
<!-- @generated:routes START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 파일 | URL prefix |
|
||||
|---|---|---|
|
||||
| `api` | `src/routes/api.php` | `/api/plugins/sirsoft-pay_nhnkcp/...` |
|
||||
| `web` | `src/routes/web.php` | `/plugins/sirsoft-pay_nhnkcp/...` |
|
||||
|
||||
확장 라우트는 **활성 상태인 확장의 것만** 등록됩니다. 라우트 정의를 바꾸면 라우트 캐시 재생성이 필요합니다.
|
||||
<!-- @generated:routes END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`api`(Bearer 토큰 인증, 모바일 승인키 발급처럼 로그인 사용자가 브라우저에서 직접 호출하는
|
||||
엔드포인트)와 `web`(콜백·입금통보·공통통보처럼 KCP 서버나 리다이렉트로 도달하는
|
||||
엔드포인트)이 분리된 이유는 인증 방식이 다르기 때문입니다 — KCP 는 우리 서비스의 Bearer
|
||||
토큰을 모르므로 콜백 라우트에 `api` 인증 미들웨어를 걸 수 없습니다. 새 KCP 콜백을 추가할
|
||||
때는 `web` 쪽에 둡니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 의존 관계
|
||||
|
||||
<!-- @generated:dependencies START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
| 확장 | 유형 | 버전 제약 | 번들 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-ecommerce` | 모듈 | `>=1.1.0` | ✅ |
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
없음.
|
||||
<!-- @generated:dependencies END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`sirsoft-ecommerce >=1.1.0` 하드 의존은 §data-model.md 에서 설명한 구조(결제 상태는
|
||||
이커머스가 소유, 이 플러그인은 절차만 소유)의 직접적 결과입니다 — 이커머스 없이는 이
|
||||
플러그인이 다룰 주문 자체가 존재하지 않습니다. 이커머스의 PG 등록 훅이나 `Order` 모델
|
||||
구조가 바뀌면 이 최소 버전을 올려야 합니다(§CLAUDE.md "확장 → 확장 동기화").
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,169 @@
|
||||
# 나이스페이먼츠 — 에이전트 가이드
|
||||
|
||||
> 이 문서는 이 플러그인을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 [README.md](README.md) 를 보세요.
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 유형: 플러그인 (sirsoft-pay_nicepayments) — 나이스페이먼츠 PG 연동(인증+승인 2단계/가상계좌/에스크로/간편결제 8종). 소유 테이블 없음 — 상태는 sirsoft-ecommerce 소유
|
||||
2. 확장 방식: `RegisterPgProviderListener`/`RegisterEasyPayMethodsListener` 로 이커머스 레지스트리에 등록 — 이커머스 코드는 이 플러그인을 모른다
|
||||
3. 건드리면 안 되는 것: 결제창 인증 실패(`AuthResultCode != '0000'`)를 승인 API 호출 전인데도 hard failure 로 취급, 가상계좌 입금통보 IP 화이트리스트(`VbankNotifyIpWhitelist`) 미부착
|
||||
4. 작업 위치: `plugins/_bundled/sirsoft-pay_nicepayments` — 활성 디렉토리 직접 수정 금지
|
||||
5. 반영: `php artisan plugin:update sirsoft-pay_nicepayments --force`
|
||||
```
|
||||
|
||||
## 1. 이 확장은 무엇인가
|
||||
|
||||
<!-- @intent START -->
|
||||
나이스페이먼츠 PG를 `sirsoft-ecommerce`에 연결하는 어댑터입니다. 결제 승인은 **인증→승인
|
||||
2단계**입니다 — 결제창(`goPay` iframe 팝업/모바일 폼)이 먼저 인증 결과(`AuthResultCode`)를
|
||||
`/payment/callback`으로 POST 하고, 서버가 그 결과를 받아 `NextAppURL`로 다시 승인 API를
|
||||
호출해야 최종 완료됩니다. `sirsoft-pay_kginicis`(결제창 후 단일 승인 API)와 달리 인증
|
||||
단계에서 실패하는 것과 승인 단계에서 실패하는 것을 서로 다르게 취급해야 합니다(§금지 패턴).
|
||||
|
||||
**설계 원칙**: 이 플러그인도 상태를 소유하지 않습니다(§data-model.md — 모델·테이블·Repository
|
||||
0개). 주문·결제 상태는 `sirsoft-ecommerce`에 있고, 이 플러그인은 그 상태를 나이스페이먼츠
|
||||
API 와 동기화하는 역할만 합니다. 등록은 훅 기반입니다
|
||||
(`sirsoft-ecommerce.payment.registered_pg_providers` 필터).
|
||||
|
||||
**의도적으로 하지 않는 것**: 사용자가 결제창에서 취소하거나 PG 가 인증을 거부한 경우
|
||||
(`AuthResultCode != '0000'`)는 아직 승인 API 호출 전이므로 일반 오류 메시지를 띄우지 않고
|
||||
체크아웃으로 조용히 리다이렉트합니다 — 이 시점은 "결제 시도 자체를 안 한 것"과 사실상
|
||||
같아서, 사용자에게 오류로 보이면 혼란만 커집니다. 운영 가시성은 로그(`auth_result_code`/
|
||||
`auth_result_msg`)로만 보존합니다. 반면 2단계(승인) 이후의 실패(서명·MID·금액 불일치)는
|
||||
"돈이 오갔을 수 있는" 실패라 `?error=` 쿼리로 명시적으로 안내합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 2. 디렉토리 지도
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-pay_nicepayments --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-pay_nicepayments --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-pay_nicepayments --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-pay_nicepayments --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-pay_nicepayments --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
|
||||
## 3. 핵심 흐름
|
||||
|
||||
<!-- @intent START -->
|
||||
**PC/모바일 결제 승인**: 결제창(iframe 팝업)이 `/payment/callback`으로 인증 결과 POST →
|
||||
`AuthResultCode == '0000'` 확인(아니면 §1 "의도적으로 하지 않는 것"의 silent redirect) →
|
||||
`sirsoft-pay_nicepayments.payment.before_authorize` 훅 → 서버가 `NextAppURL`로 승인 API
|
||||
호출 → `sirsoft-pay_nicepayments.payment.after_authorize` 훅 → 이커머스 주문 결제 완료
|
||||
처리. 결제 요청 시점에 주문의 `total_tax_amount`/`total_vat_amount`/`total_tax_free_amount`
|
||||
가 모두 0이 아니면 과세 필드를 폼에 포함합니다(§4 "과세 처리").
|
||||
|
||||
**결제 취소(환불)**: 관리자가 주문 취소(`cancel_pg=true`) → 코어가
|
||||
`sirsoft-ecommerce.payment.refund` 필터 발화 → `PaymentRefundListener`(우선순위 10)가 먼저
|
||||
나이스페이먼츠 취소 API 호출(전액취소 `isPartial=0`/부분취소 `isPartial=1`) →
|
||||
`CancelActivityLogListener`(우선순위 20)가 결과를 활동 로그에 별도 기록. 가상계좌 입금
|
||||
완료 건은 환불 계좌 정보가 필요해 일반 취소 API가 아니라 별도 어드민 환불 계좌 API 경로로
|
||||
처리됩니다. 취소 API 호출이 실패하면 `refund_failed` 훅이 발화합니다.
|
||||
|
||||
**에스크로 배송 등록**: 관리자 주문 상세에서 운송장번호·택배사를 입력 →
|
||||
`AdminEscrowController::registerDelivery()` → 나이스페이먼츠 배송 등록 API 호출.
|
||||
`EscrowDeliveryRegisterRequest`가 입력을 검증합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 4. 확장점
|
||||
|
||||
<!-- @generated:extension-points-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 확장점 | 수 | 상세 |
|
||||
|---|---|---|
|
||||
| 발행 훅 | 5개 | [발행 훅](docs/extension-points.md#발행-훅) |
|
||||
| 구독 훅 | 9개 | [구독 훅](docs/extension-points.md#구독-훅) |
|
||||
| 훅 리스너 | 8개 | [훅 리스너](docs/extension-points.md#훅-리스너) |
|
||||
| 레이아웃 확장 | 5개 | [레이아웃 확장](docs/extension-points.md#레이아웃-확장) |
|
||||
| 미들웨어 | 1개 | [미들웨어](docs/extension-points.md#미들웨어) |
|
||||
| 브로드캐스트 채널 | 0개 | [브로드캐스트 채널](docs/extension-points.md#브로드캐스트-채널) |
|
||||
| 스케줄 | 0개 | [스케줄](docs/extension-points.md#스케줄) |
|
||||
| 알림 정의 | 0개 | [알림 정의](docs/extension-points.md#알림-정의) |
|
||||
<!-- @generated:extension-points-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`before_authorize`/`before_cancel`은 API 호출 **전** 개입 지점이라 여기서 예외를 던지면
|
||||
실제 나이스페이먼츠 호출이 일어나지 않습니다. `refund_failed`는 취소 API 호출이 실패했을
|
||||
때만 발화하는 별도 훅입니다 — `after_cancel`(성공 응답 후)과 구분해서 구독해야 합니다.
|
||||
운영자 알림(예: Slack) 을 붙이고 싶은 확장은 `after_*`가 아니라 `refund_failed`를 잡습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 5. 수정 시 동반 의무
|
||||
|
||||
- [ ] `_bundled` 에서만 수정하고 `php artisan plugin:update sirsoft-pay_nicepayments --force` 로 반영
|
||||
- [ ] manifest version 상향 시 `package.json` · `package-lock.json` · `composer.json` 동기화 + CHANGELOG 기재
|
||||
- [ ] 발행 훅 추가·이름 변경 시 `php artisan ext:docgen` 재실행 (구독하는 확장의 계약이 바뀝니다)
|
||||
- [ ] API 표면 변경 시 `php artisan api:docgen --scope=plugin:sirsoft-pay_nicepayments` 재실행 + `docs/api/**` 갱신
|
||||
- [ ] 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
|
||||
- [ ] TSX/TS 변경 시 `--production` 재빌드 후 `dist/` 커밋 (sourceMappingURL 잔존 금지)
|
||||
- [ ] 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
|
||||
- [ ] 승인/취소 흐름을 고칠 때 `before_*`/`after_*` 훅 순서와 우선순위(`PaymentRefundListener` < `CancelActivityLogListener`)를 유지 — 로그가 실제 처리보다 먼저 실행되면 안 된다
|
||||
- [ ] 인증 실패(1단계)와 승인 실패(2단계)의 사용자 안내 방식(silent redirect vs `?error=`)을 구분 유지 — §1 "의도적으로 하지 않는 것" 참고
|
||||
- [ ] IP 화이트리스트(`VbankNotifyIpWhitelist`) 대상 라우트를 추가/변경하면 미들웨어 부착 대상(targets)도 함께 갱신
|
||||
- [ ] 새 간편결제 수단을 추가하면 그 결제수단의 계약 상태를 관리자 안내에도 반영
|
||||
|
||||
## 6. 금지 패턴
|
||||
|
||||
<!-- @intent START -->
|
||||
| 금지 | 올바른 사용 | 이유 |
|
||||
|---|---|---|
|
||||
| 결제창 인증 실패(`AuthResultCode != '0000'`)를 승인 실패와 동일하게 `?error=` 로 안내 | 승인 API 호출 전 실패는 체크아웃으로 silent redirect, 로그로만 기록 | 아직 결제 시도 자체가 성립하지 않은 단계인데 오류 메시지를 띄우면 사용자가 "돈이 빠져나갔나" 불필요하게 불안해한다 |
|
||||
| 가상계좌 입금통보(`vbank-notify`)에 IP 화이트리스트 미부착 | `VbankNotifyIpWhitelist` 미들웨어 유지 | 통보 엔드포인트는 나이스페이먼츠 서버만 호출해야 하며, 화이트리스트가 없으면 제3자가 위조 입금통보를 보내 결제 상태를 조작할 수 있다 |
|
||||
| 부분취소인데 가상계좌 입금 완료 건을 일반 취소 API로 처리 | 환불 계좌 정보가 필요한 가상계좌 건은 별도 어드민 환불 계좌 API 경로로 처리 | 가상계좌는 카드와 달리 PG가 자동으로 환불할 계좌를 모르므로 일반 취소 API를 호출하면 실패하거나 환불이 누락된다 |
|
||||
| 라이브 가맹점 키를 로그·에러 메시지에 노출 | 운영 키는 항상 마스킹하거나 로그 대상에서 제외 | 노출되면 제3자가 결제 요청을 위조할 수 있다 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 7. 테스트 실행
|
||||
|
||||
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 개수 | 위치 |
|
||||
|---|---|---|
|
||||
| PHPUnit | 23개 | `plugins/_bundled/sirsoft-pay_nicepayments/tests` |
|
||||
| Vitest | 7개 | `vitest.config.ts` |
|
||||
| Playwright | 0개 | — |
|
||||
| 시나리오 매니페스트 | 1개 | `tests/scenarios` |
|
||||
|
||||
기저 TestCase: `tests/PluginTestCase.php` — 확장 테스트는 이 클래스를 상속합니다 (`Tests\TestCase` 직접 상속 금지).
|
||||
|
||||
```bash
|
||||
# PHPUnit (변경 범위만) (Bash)
|
||||
php vendor/bin/phpunit plugins/_bundled/sirsoft-pay_nicepayments/tests --filter='<대상클래스>'
|
||||
|
||||
# Vitest (확장 디렉토리에서) (PowerShell)
|
||||
cd plugins/_bundled/sirsoft-pay_nicepayments && powershell -Command "npm run test:run -- <대상>"
|
||||
|
||||
```
|
||||
|
||||
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
|
||||
<!-- @generated:test-commands END -->
|
||||
|
||||
## 8. 문서 목차
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
@@ -1,146 +1,236 @@
|
||||
# NicePayments Plugin for G7
|
||||
# NicePayments
|
||||
|
||||
나이스페이먼츠(NicePayments) PG 연동 플러그인입니다. G7 플랫폼의 sirsoft-ecommerce 모듈과 함께 동작합니다.
|
||||
**G7 플러그인 · sirsoft-pay_nicepayments**
|
||||
나이스페이먼츠 결제를 sirsoft-ecommerce 에 연결하는 결제 플러그인
|
||||
|
||||
## 지원 결제 수단
|
||||
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-1.0.2-0066FF?style=flat-square" alt="version 1.0.2">
|
||||
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
|
||||
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 >=7.0.10">
|
||||
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
|
||||
<img src="https://img.shields.io/badge/requires-sirsoft--ecommerce-BF8700?style=flat-square" alt="requires sirsoft-ecommerce">
|
||||
</p>
|
||||
<!-- @generated:badges END -->
|
||||
|
||||
| 결제 수단 | PayMethod |
|
||||
|-----------|-----------|
|
||||
| 신용카드 | CARD |
|
||||
| 가상계좌 | VBANK |
|
||||
| 계좌이체 | BANK |
|
||||
| 휴대폰결제 | CELLPHONE |
|
||||
---
|
||||
|
||||
[소개](#소개) · [주요 기능](#주요-기능) · [동작 방식](#동작-방식) · [요구 사항](#요구-사항) · [설치](#설치) · [관리자 설정](#관리자-설정) · [사용 방법](#사용-방법) · [다른 확장과의 연동](#다른-확장과의-연동) · [문서](#문서) · [트러블슈팅](#트러블슈팅) · [변경 이력](#변경-이력) · [라이선스](#라이선스)
|
||||
|
||||
---
|
||||
|
||||
## 소개
|
||||
|
||||
<!-- @intent START -->
|
||||
나이스페이먼츠(NicePayments) 표준결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제
|
||||
플러그인입니다. 결제 승인은 **인증→승인 2단계**로 나뉩니다 — 결제창(`goPay` iframe
|
||||
팝업/모바일 폼)이 먼저 인증 결과를 서버로 보내고, 서버가 그 결과를 받아 별도 승인 API를
|
||||
호출해야 최종 완료됩니다.
|
||||
|
||||
이 플러그인은 결제 자체의 상태(주문·결제 성공/실패/취소)를 소유하지 않습니다 — 그 상태는
|
||||
`sirsoft-ecommerce`의 주문·결제 테이블에 있고, 이 플러그인은 "그 상태를 나이스페이먼츠
|
||||
API 와 어떻게 주고받는가"만 책임집니다. 그래서 이 플러그인은 소유 테이블/모델이 하나도
|
||||
없습니다(§data-model.md).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 주요 기능
|
||||
|
||||
<!-- @intent START -->
|
||||
| 영역 | 설명 |
|
||||
|---|---|
|
||||
| 결제수단 | 신용카드, 계좌이체, 가상계좌, 휴대폰결제 |
|
||||
| 간편결제 | 네이버페이, 카카오페이, 삼성페이, 애플페이, PAYCO, 11pay, SSG페이, L.pay 버튼 주입 |
|
||||
| 승인 방식 | 결제창 인증 + 서버 승인 API 2단계 |
|
||||
| 가상계좌 | 발급 + 입금통보 처리 |
|
||||
| 에스크로 | 배송 등록, 거래 조회 |
|
||||
| 결제 취소 | 전액/부분취소, PG 취소 확인 시점 별도 활동 로그(PG 응답 시각·취소 TID), 실패 시 `refund_failed` 훅 |
|
||||
| 영수증 | 주문 완료/마이페이지 영수증 버튼 |
|
||||
| 관리자 확장 | 주문 상세 거래 조회, 에스크로 배송 등록 UI |
|
||||
| 과세 처리 | 주문의 세금/부가세/면세 금액 자동 반영 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 동작 방식
|
||||
|
||||
<!-- @intent START -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[체크아웃 주문 생성] --> B["결제창(goPay iframe) 로드"]
|
||||
B --> C["/payment/callback (AuthResultCode)"]
|
||||
C -->|실패| D[체크아웃으로 silent redirect]
|
||||
C -->|성공| E["NextAppURL 로 승인 API 호출"]
|
||||
E --> F[주문 결제 완료 처리]
|
||||
F --> G[성공 URL 리다이렉트]
|
||||
```
|
||||
|
||||
결제창에서 사용자가 취소하거나 PG 가 인증을 거부하면(`AuthResultCode != '0000'`) 아직
|
||||
승인 API 호출 전이므로 일반 오류 메시지를 띄우지 않고 체크아웃으로 조용히 리다이렉트합니다
|
||||
— 운영 가시성은 로그(`auth_result_code`/`auth_result_msg`)로 보존합니다. 2단계(승인) 이후의
|
||||
실패(서명/MID/금액 불일치)는 `?error=` 쿼리로 명시적으로 안내합니다.
|
||||
|
||||
가상계좌는 결제창에서 발급되면 주문이 입금대기 상태로 유지되다가, 나이스페이먼츠가 입금통보
|
||||
URL로 결과를 POST 하면 결제 완료 처리됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 요구 사항
|
||||
|
||||
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| G7 코어 | `>=7.0.10` |
|
||||
| PHP | `^8.2` |
|
||||
| 의존 모듈 | `sirsoft-ecommerce` `>=1.1.0` |
|
||||
<!-- @generated:requirements END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
| 항목 | 필요한 것 |
|
||||
|---|---|
|
||||
| 운영 환경 | HTTPS 도메인, 올바른 `APP_URL`, 나이스페이먼츠 가맹점 계약 정보 |
|
||||
| PC/모바일 결제 | MID, 가맹점 키 |
|
||||
|
||||
서버에서 나이스페이먼츠 API 호스트로 HTTPS outbound 요청이 가능해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 설치
|
||||
|
||||
<!-- @generated:install START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
```bash
|
||||
# 플러그인 디렉토리에 배치 후
|
||||
composer install
|
||||
npm install && npm run build
|
||||
# 번들 설치 (코어에 동봉된 소스에서 설치)
|
||||
php artisan plugin:install sirsoft-pay_nicepayments
|
||||
|
||||
# 활성화
|
||||
php artisan plugin:activate sirsoft-pay_nicepayments
|
||||
|
||||
# 업데이트 (번들 소스 기준 강제 반영)
|
||||
php artisan plugin:update sirsoft-pay_nicepayments --force
|
||||
```
|
||||
|
||||
## 설정
|
||||
저장소: https://github.com/gnuboard/g7-plugin-sirsoft-pay_nicepayments
|
||||
<!-- @generated:install END -->
|
||||
|
||||
관리자 → 플러그인 → NicePayments 설정에서 구성합니다.
|
||||
설치·활성화 후 이커머스 결제 설정에서 PG 제공자를 "나이스페이먼츠"로 선택해야 실제로 결제
|
||||
흐름에 연결됩니다 — 활성화만으로는 체크아웃 화면에 나타나지 않습니다.
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| 테스트 모드 | 활성화 시 나이스페이먼츠 공용 테스트 MID를 사용합니다. 실제 카드 승인/출금 알림이 발생할 수 있으며, 테스트 계정 결제는 당일 23:30경 일괄 자동 취소됩니다. |
|
||||
| 테스트 MID | 테스트 가맹점 ID (`nicepay00m` 기본값) |
|
||||
| 테스트 가맹점 키 | 나이스페이 공용 테스트 키 |
|
||||
| 라이브 MID | 실서비스 가맹점 ID |
|
||||
| 라이브 가맹점 키 | 실서비스 가맹점 키 (외부 노출 금지) |
|
||||
| 결제 성공 URL | 결제 완료 후 리다이렉트 경로 (`{orderId}` 치환 지원) |
|
||||
| 결제 실패 URL | 결제 실패 후 리다이렉트 경로 |
|
||||
## 관리자 설정
|
||||
|
||||
테스트 모드 주문은 실제 배송하지 마세요. 실제 카드 승인/출금 알림이 발생할 수 있으며, 테스트 계정 결제는 당일 23:30경 일괄 자동 취소됩니다.
|
||||
<!-- @generated:settings-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 의미 | 기본값 |
|
||||
|---|---|---|
|
||||
| `is_test_mode` | 테스트 모드 | `true` |
|
||||
| `test_mid` | 테스트 가맹점 ID (MID) | `nicepay00m` |
|
||||
| `test_merchant_key` | 테스트 가맹점 키 | `EYzu8jGGMfqaDEp76gSckuvnaHHu+bC4opsSN6lHv3b2lurNYkVXrZ7Z1AoqQnXI3eLuaUFyoRNC6FkrzVjceg==` |
|
||||
| `live_mid` | 라이브 가맹점 ID (MID) | - |
|
||||
| `live_merchant_key` | 라이브 가맹점 키 | - |
|
||||
| `redirect_success_url` | 결제 성공 리다이렉트 URL | `{shopBase}/orders/{orderId}/complete` |
|
||||
| `redirect_fail_url` | 결제 실패 리다이렉트 URL | `{shopBase}/checkout` |
|
||||
| `use_escrow` | 에스크로 결제 사용 | `false` |
|
||||
| `easy_pay_allow_with_other_pg` | 타 PG와 사용가능함 | `false` |
|
||||
| `easy_pay_naverpay` | 네이버페이 간편결제 | `false` |
|
||||
| `easy_pay_kakaopay` | 카카오페이 간편결제 | `false` |
|
||||
| `easy_pay_samsungpay` | 삼성페이 간편결제 | `false` |
|
||||
| `easy_pay_applepay` | 애플페이 간편결제 | `false` |
|
||||
| `easy_pay_payco` | PAYCO 간편결제 | `false` |
|
||||
| `easy_pay_skpay` | 11pay (SK페이) 간편결제 | `false` |
|
||||
| `easy_pay_ssgpay` | SSG페이 간편결제 | `false` |
|
||||
| `easy_pay_lpay` | L.pay 간편결제 | `false` |
|
||||
|
||||
## 웹훅 (가상계좌 입금 통보)
|
||||
개발자용 상세(타입·검증·저장 위치)는 [설정 스키마](docs/settings.md#설정-스키마) 를 보세요.
|
||||
<!-- @generated:settings-summary END -->
|
||||
|
||||
나이스페이먼츠 관리자에서 가상계좌 입금 통보 URL을 아래로 설정하세요:
|
||||
<!-- @intent START -->
|
||||
테스트 모드에서는 실제 카드 승인/출금 알림이 발생할 수 있으며, 테스트 계정 결제는 당일
|
||||
23:30경 일괄 자동 취소될 수 있습니다 — **테스트 모드 주문을 실제로 배송하지 마세요.** 라이브
|
||||
가맹점 키는 외부에 노출하지 마세요.
|
||||
|
||||
```
|
||||
**웹훅(가상계좌 입금 통보)** — 나이스페이먼츠 관리자에 아래 URL을 실제 운영 도메인으로
|
||||
등록합니다.
|
||||
|
||||
```text
|
||||
https://your-domain.com/plugins/sirsoft-pay_nicepayments/payment/vbank-notify
|
||||
```
|
||||
|
||||
### IP 화이트리스트
|
||||
|
||||
나이스페이먼츠 서버 IP만 허용됩니다. 로컬/테스트 환경에서는 자동으로 우회됩니다.
|
||||
가상계좌 입금 통보는 나이스페이먼츠 서버가 직접 호출하므로 아래 IP 화이트리스트가
|
||||
적용됩니다(로컬/테스트 환경에서는 자동 우회).
|
||||
|
||||
| IP |
|
||||
|----|
|
||||
| 121.133.126.10 |
|
||||
| 121.133.126.11 |
|
||||
| 211.33.136.39 |
|
||||
| `121.133.126.10` |
|
||||
| `121.133.126.11` |
|
||||
| `211.33.136.39` |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 결제 흐름
|
||||
## 사용 방법
|
||||
|
||||
### PC / 모바일 결제 (인증 + 승인 2단계)
|
||||
<!-- @intent START -->
|
||||
**결제 취소/부분취소**: 관리자가 주문 취소를 요청(`cancel_pg=true`)하면 코어가
|
||||
`sirsoft-ecommerce.payment.refund` 필터 훅을 발화하고, `PaymentRefundListener`가
|
||||
나이스페이먼츠 취소 API를 호출합니다(전액취소 `isPartial=0`/부분취소 `isPartial=1`).
|
||||
배송비가 포함된 주문은 전체취소 시 배송비도 함께 환불 레코드에 반영되고, 쿠폰이 적용된
|
||||
주문은 실결제금액(쿠폰 차감 후)이 PG `cancelAmt`로 전달됩니다. 부분취소로 쿠폰 최소
|
||||
주문금액 조건을 더 이상 충족하지 못하면 코어가 취소 자체를 거부(422)해 PG 호출이 아예
|
||||
발생하지 않습니다. 가상계좌 입금 완료 건은 환불 계좌 정보가 필요해 일반 취소 API가 아닌
|
||||
별도 어드민 환불 계좌 API 경로로 처리됩니다.
|
||||
|
||||
```
|
||||
브라우저 → goPay(form) / 모바일 결제창 form POST
|
||||
결제창 → POST /payment/callback → authCallback() (1단계 인증)
|
||||
서버 → POST NextAppURL → 승인 API 호출 (2단계)
|
||||
승인 완료 → completePayment() → 성공 페이지 리다이렉트
|
||||
```
|
||||
**에스크로 배송 등록**: 관리자 주문 상세에서 운송장번호·택배사를 입력해 나이스페이먼츠
|
||||
배송 등록을 호출할 수 있습니다.
|
||||
|
||||
### 결제창 취소 / 인증 실패
|
||||
**거래 단건 조회**: `NicePaymentsApiService::queryTransaction(string $tid): array`로 거래
|
||||
상태를 조회할 수 있습니다.
|
||||
|
||||
모바일 결제창에서 사용자가 취소버튼을 누르거나 PG 가 인증을 거부하면 (AuthResultCode != '0000') 결제 승인 (NextAppURL 호출) 이전이므로 사용자에게 generic 오류 메시지를 띄우지 않고 체크아웃으로 silent redirect 합니다. 운영 가시성은 로그(`auth_result_code` / `auth_result_msg`) 로 보존됩니다. 2단계 이후 hard failure (signature / mid / amount / authorize) 는 종전대로 `?error=` 쿼리 부착하여 안내합니다.
|
||||
전체 API 목록(사용자/관리자)은 [docs/api/](docs/api/README.md) 를, 발행/구독 훅 목록은
|
||||
[docs/extension-points.md](docs/extension-points.md) 를 참고하세요.
|
||||
<!-- @intent END -->
|
||||
|
||||
### 결제 취소 / 부분취소
|
||||
## 다른 확장과의 연동
|
||||
|
||||
```text
|
||||
관리자 주문 취소 요청 (cancel_pg=true)
|
||||
→ 코어가 sirsoft-ecommerce.payment.refund 필터 훅 발화
|
||||
→ PaymentRefundListener 가 NicePayments cancelPayment API 호출
|
||||
· 전액취소: isPartial=0
|
||||
· 부분취소: isPartial=1
|
||||
→ 코어가 환불 레코드 생성 + 쿠폰 / 마일리지 / 재고 복원
|
||||
→ CancelActivityLogListener 가 PG 응답 시각·취소 TID를 활동 로그에 기록
|
||||
```
|
||||
<!-- @generated:integrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
배송비가 포함된 주문은 전체취소 시 배송비도 함께 환불 레코드에 반영되고, 쿠폰이 적용된 주문은 실결제금액(쿠폰 차감 후) 이 PG cancelAmt 로 전달됩니다. 부분취소 시 쿠폰 최소 주문금액 조건을 더 이상 충족하지 못하면 코어가 취소 자체를 거부 (422) 하여 PG 호출이 발생하지 않습니다. 가상계좌 입금 완료 건은 환불 계좌 정보가 필요해 일반 취소 API 가 아닌 별도 어드민 환불 계좌 API 경로로 처리됩니다.
|
||||
| 확장 | 유형 | 버전 제약 | 번들 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-ecommerce` | 모듈 | `>=1.1.0` | ✅ |
|
||||
|
||||
## 가용 훅 (Hook)
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
다른 플러그인이나 리스너에서 아래 훅에 연결할 수 있습니다.
|
||||
없음.
|
||||
<!-- @generated:integrations END -->
|
||||
|
||||
### 액션 훅
|
||||
<!-- @intent START -->
|
||||
`RegisterPgProviderListener`가 이 플러그인을 이커머스의 PG 제공자 레지스트리에,
|
||||
`RegisterEasyPayMethodsListener`가 간편결제 결제수단 레지스트리에 각각 등록합니다 — PG
|
||||
결제사 선택과 간편결제 노출은 서로 독립적이라, 다른 PG가 기본값이어도 나이스페이먼츠
|
||||
간편결제 버튼을 체크아웃 화면에 노출하는 조합이 가능합니다(`easy_pay_allow_with_other_pg`).
|
||||
<!-- @intent END -->
|
||||
|
||||
| 훅 이름 | 시점 | 인수 |
|
||||
|---------|------|------|
|
||||
| `sirsoft-pay_nicepayments.payment.before_authorize` | 서버 승인 API 호출 직전 | `Order $order, array $pgParams` |
|
||||
| `sirsoft-pay_nicepayments.payment.after_authorize` | 서버 승인 API 응답 직후 | `Order $order, array $pgResponse` |
|
||||
| `sirsoft-pay_nicepayments.payment.before_cancel` | NicePayments 취소 API 호출 직전 | `Order $order, OrderPayment $payment, float $refundAmount` |
|
||||
| `sirsoft-pay_nicepayments.payment.after_cancel` | NicePayments 취소 API 호출 직후 | `Order $order, OrderPayment $payment, array $pgResponse` |
|
||||
| `sirsoft-pay_nicepayments.payment.refund_failed` | 환불 API 호출 실패 시 | `Order $order, OrderPayment $payment, array $context` |
|
||||
## 문서
|
||||
|
||||
#### `refund_failed` context 구조
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
|
||||
```php
|
||||
[
|
||||
'tid' => string, // 나이스페이 거래번호
|
||||
'cancel_amt' => int, // 환불 시도 금액 (원)
|
||||
'error' => string, // 오류 메시지
|
||||
]
|
||||
```
|
||||
## 트러블슈팅
|
||||
|
||||
### 훅 등록 예시
|
||||
<!-- @intent START -->
|
||||
| 증상 | 원인 | 조치 |
|
||||
|---|---|---|
|
||||
| 결제창에서 취소했는데 오류 화면이 뜸 | `AuthResultCode != '0000'` 분기가 silent redirect 대신 오류를 노출하도록 잘못 수정됨 | §동작 방식의 의도된 동작(조용히 체크아웃 복귀)으로 되돌리고 로그로만 확인 |
|
||||
| 가상계좌 입금통보가 반영되지 않음 | 운영 환경 IP 화이트리스트에 나이스페이먼츠 통보 서버 IP가 없음 | 위 IP 목록으로 화이트리스트를 갱신 |
|
||||
| 결제 승인 후 주문이 실패 상태로 남음 | 인증은 성공했지만 승인 API 호출 또는 로컬 후속 처리 실패 | 오류 로그 확인 — PG 승인은 이미 됐을 수 있으므로 수동 확인 필요 |
|
||||
| 부분취소가 실패하고 422 응답 | 부분취소로 쿠폰 최소 주문금액 조건 미충족 | 코어가 의도적으로 거부한 것 — 쿠폰 조건을 다시 충족하거나 전액취소 |
|
||||
| 결제 성공했는데 간편결제 버튼 클릭 시 오류 | 나이스페이먼츠 계약이 없는 결제수단/간편결제를 활성화 | 계약이 완료된 결제수단만 관리자 설정에서 활성화 |
|
||||
<!-- @intent END -->
|
||||
|
||||
```php
|
||||
use App\Extension\HookManager;
|
||||
## 변경 이력
|
||||
|
||||
HookManager::addAction(
|
||||
'sirsoft-pay_nicepayments.payment.refund_failed',
|
||||
function (Order $order, OrderPayment $payment, array $context) {
|
||||
// 예: Slack 알림 발송
|
||||
SlackNotifier::send("환불 실패: 주문 #{$order->order_number}, 오류: {$context['error']}");
|
||||
},
|
||||
priority: 10
|
||||
);
|
||||
```
|
||||
|
||||
## API 단건 조회
|
||||
|
||||
`NicePaymentsApiService::queryTransaction(string $tid): array` 메서드로 거래 상태를 조회할 수 있습니다.
|
||||
|
||||
```php
|
||||
$apiService = app(\Plugins\Sirsoft\PayNicepayments\Services\NicePaymentsApiService::class);
|
||||
$result = $apiService->queryTransaction('NICE_TID_12345');
|
||||
// $result['ResultCode'], $result['Amt'], ...
|
||||
```
|
||||
|
||||
## 과세 처리
|
||||
|
||||
결제 요청 시 주문의 `total_tax_amount`, `total_vat_amount`, `total_tax_free_amount` 값을 자동으로 나이스페이 폼에 포함합니다. 세 값이 모두 0이면 과세 필드를 생략합니다.
|
||||
|
||||
## 테스트 실행
|
||||
|
||||
```bash
|
||||
cd c:/g7
|
||||
php artisan test --filter=Nicepayments
|
||||
```
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 라이선스
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 나이스페이먼츠 개발자 문서
|
||||
|
||||
> plugins/_bundled/sirsoft-pay_nicepayments · 플러그인
|
||||
|
||||
<!-- @generated:stats START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**훅 수**: 5 · **구독 훅 수**: 9 · **라우트 수**: 15 · **모델 수**: 0 · **테이블 수**: 0 · **마이그레이션 수**: 0 · **레이아웃 수**: 1 · **핸들러 수**: 2
|
||||
<!-- @generated:stats END -->
|
||||
|
||||
## 문서 목차
|
||||
|
||||
<!-- @generated:doc-toc START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | 설계 의도·계층 지도·디렉토리 맵 |
|
||||
| [extension-points.md](extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 |
|
||||
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
|
||||
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
|
||||
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
|
||||
| [api/](api/README.md) | API 레퍼런스 |
|
||||
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
|
||||
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
|
||||
<!-- @generated:doc-toc END -->
|
||||
@@ -0,0 +1,60 @@
|
||||
# 나이스페이먼츠 — 아키텍처
|
||||
|
||||
> 설계 의도와 계층 구조 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설계 의도
|
||||
|
||||
<!-- @intent START -->
|
||||
나이스페이먼츠 표준결제를 `sirsoft-ecommerce` 에 연결하는 어댑터입니다. 이 플러그인의
|
||||
고유한 설계 축은 승인이 **인증→승인 2단계**로 나뉜다는 점입니다 — 결제창이 먼저 인증
|
||||
결과만 보내고, 서버가 그 결과를 별도 승인 API 호출로 확정합니다. `sirsoft-pay_kginicis`처럼
|
||||
결제창 콜백 하나로 승인까지 끝나는 구조와 달리, 이 플러그인은 "인증은 됐지만 아직 승인
|
||||
전"이라는 중간 상태를 다뤄야 합니다(§AGENTS.md "의도적으로 하지 않는 것").
|
||||
|
||||
이 플러그인도 결제 상태 자체는 소유하지 않습니다 — 주문·결제 테이블은 `sirsoft-ecommerce`
|
||||
소유이고, 이 플러그인은 그 상태를 나이스페이먼츠 API 와 어떻게 주고받는가만 책임집니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 계층 지도
|
||||
|
||||
<!-- @intent START -->
|
||||
```text
|
||||
Controller (PaymentCallbackController / AdminEscrowController / AdminTransactionController)
|
||||
→ NicePaymentsApiService (인증 검증 · 승인/취소/단건조회 API 호출)
|
||||
→ sirsoft-ecommerce 의 Order/OrderPayment 모델 (직접 참조 — 이 플러그인 소유 모델 없음)
|
||||
|
||||
Listener (RegisterPgProviderListener 등)
|
||||
→ sirsoft-ecommerce 의 필터 훅에 등록 (컴파일 타임 결합 없음)
|
||||
```
|
||||
|
||||
`PaymentCallbackController`가 인증 결과(`AuthResultCode`)를 먼저 판정하고 실패 시 승인
|
||||
API 호출 없이 조기 반환하는 것이 이 플러그인 계층 구조의 특징입니다 — 일반적인 "Controller
|
||||
→ FormRequest → Service" 흐름과 달리, 이 판정 자체가 Controller 안에 있는 이유는 그
|
||||
판정 결과에 따라 아예 다른 응답(silent redirect vs `?error=`)을 골라야 하기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 디렉토리
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-pay_nicepayments --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-pay_nicepayments --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-pay_nicepayments --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-pay_nicepayments --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-pay_nicepayments --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
@@ -0,0 +1,65 @@
|
||||
# 나이스페이먼츠 — 데이터 모델
|
||||
|
||||
> 모델·소유 테이블·마이그레이션·Enum · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 모델
|
||||
|
||||
<!-- @generated:models START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_소유 모델이 없습니다._
|
||||
<!-- @generated:models END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 상태는 이 플러그인이 아니라 `sirsoft-ecommerce`의 `Order`/`OrderPayment` 모델이
|
||||
소유합니다(§AGENTS.md "설계 원칙"). 이 플러그인은 그 모델을 직접 참조해 읽고 쓸 뿐, 자기
|
||||
Repository 조차 두지 않았습니다(§Repository) — 인증→승인 2단계 흐름의 중간 상태도 별도
|
||||
테이블에 저장하지 않고, 인증 결과를 받은 요청 컨텍스트 안에서만 다루다가 승인이 확정되는
|
||||
순간 이커머스 테이블에 반영합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 소유 테이블
|
||||
|
||||
<!-- @generated:tables START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_소유 테이블이 없습니다._
|
||||
<!-- @generated:tables END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
가상계좌 발급 정보와 나이스페이먼츠 거래번호(TID)는 이커머스 `OrderPayment` 테이블의 기존
|
||||
컬럼/메타에 저장됩니다 — PG 마다 별도 결제상세 테이블을 두면 관리자 주문 상세가 PG
|
||||
종류에 따라 다른 테이블을 조인해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 마이그레이션
|
||||
|
||||
<!-- @generated:migrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_마이그레이션이 없습니다._
|
||||
<!-- @generated:migrations END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
소유 테이블이 없으므로(§소유 테이블) 스키마 변경 자체가 발생하지 않습니다. 설정 스키마
|
||||
변경(§settings.md)은 `config/settings/defaults.json` 갱신만으로 끝나며 DB 마이그레이션
|
||||
대상이 아닙니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Enum
|
||||
|
||||
<!-- @generated:enums START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_Enum 이 없습니다._
|
||||
<!-- @generated:enums END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`AuthResultCode`(`'0000'` = 성공)와 PG 응답 코드는 Enum 대신 컨트롤러의 조건 분기로 직접
|
||||
판정합니다 — 나이스페이먼츠 고유 프로토콜 상수라 이 플러그인 코드 어디에도 재사용되지
|
||||
않으며, Enum 으로 승격해도 얻는 타입 안전성 대비 간접 계층만 늘어납니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Repository
|
||||
|
||||
<!-- @generated:repositories START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_Repository 가 없습니다._
|
||||
<!-- @generated:repositories END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
이 플러그인이 이커머스 `Order`/`OrderPayment`를 읽고 쓰는 지점(컨트롤러·리스너)은 모두
|
||||
이커머스가 이미 노출한 Eloquent 모델을 직접 참조합니다 — 자기 소유 테이블이 없는 상태에서
|
||||
남의 모델을 감싸는 Repository 를 새로 만드는 것은 위임만 하는 빈 계층입니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,141 @@
|
||||
# 나이스페이먼츠 — 확장점
|
||||
|
||||
> 발행/구독 훅·미들웨어·채널·스케줄 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 발행 훅
|
||||
|
||||
<!-- @generated:hooks-published START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
발행 훅 5종 / 호출 지점 5곳. 이 중 1종은 `getHooks()` 선언에 없어 소스에서 자동 감지한 것입니다 — 선언에 추가하면 유형과 설명이 함께 실립니다.
|
||||
|
||||
| 훅 이름 | 유형 | 설명 | 발행 위치 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-pay_nicepayments.payment.after_authorize` | action | 나이스페이먼츠 서버 승인 완료 후 | `src/Controllers/PaymentCallbackController.php:257` |
|
||||
| `sirsoft-pay_nicepayments.payment.after_cancel` | action | 나이스페이먼츠 결제 취소 완료 후 | `src/Services/NicePaymentsApiService.php:308` |
|
||||
| `sirsoft-pay_nicepayments.payment.before_authorize` | action | 나이스페이먼츠 서버 승인 API 호출 전 | `src/Controllers/PaymentCallbackController.php:252` |
|
||||
| `sirsoft-pay_nicepayments.payment.before_cancel` | action | 나이스페이먼츠 결제 취소 API 호출 전 (본인인증 등 확장 지점) | `src/Services/NicePaymentsApiService.php:285` |
|
||||
| `sirsoft-pay_nicepayments.payment.refund_failed` | action | — | `src/Listeners/PaymentRefundListener.php:131` |
|
||||
<!-- @generated:hooks-published END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`before_authorize`/`before_cancel`은 API 호출 **전** 개입 지점입니다 — 예외를 던지면 실제
|
||||
나이스페이먼츠 호출 자체가 일어나지 않습니다. `refund_failed`는 `getHooks()` 선언 없이
|
||||
소스에서 자동 감지된 훅으로, `after_cancel`(성공 응답)과 달리 취소 API 호출이 **실패**했을
|
||||
때만 발화합니다 — 운영 알림을 붙이려는 확장은 이 훅을 구독해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 구독 훅
|
||||
|
||||
<!-- @generated:hooks-subscribed START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 훅 이름 | 유형 | 리스너 | 메서드 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| `core.layout_extension.after_apply` | filter | `AdjustEcommercePaymentMethodsLayoutListener` | `adjustPaymentMethodsLayout` | 40 |
|
||||
| `core.layout_extension.after_apply` | filter | `EnsureAdminOrderDetailPaymentQueryLayoutListener` | `ensurePaymentQueryLayout` | 66 |
|
||||
| `core.layout_extension.after_apply` | filter | `EnsureAdminOrderListTestBadgeLayoutListener` | `ensureTestBadgeLayout` | 60 |
|
||||
| `core.plugins.updated` | action | `RestoreLayoutExtensionsAfterUpdateListener` | `restoreCurrentExtensionsAfterUpdate` | 20 |
|
||||
| `sirsoft-ecommerce.payment.get_client_config` | filter | `RegisterPgProviderListener` | `getClientConfig` | 10 |
|
||||
| `sirsoft-ecommerce.payment.refund` | filter | `CancelActivityLogListener` | `logCancelConfirmed` | 20 |
|
||||
| `sirsoft-ecommerce.payment.refund` | filter | `PaymentRefundListener` | `processRefund` | 10 |
|
||||
| `sirsoft-ecommerce.payment.registered_pg_providers` | filter | `RegisterPgProviderListener` | `registerProvider` | 10 |
|
||||
| `sirsoft-ecommerce.settings.filter_available_payment_methods` | filter | `RegisterEasyPayMethodsListener` | `injectEasyPayMethods` | 40 |
|
||||
<!-- @generated:hooks-subscribed END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`PaymentRefundListener`(10)가 `CancelActivityLogListener`(20)보다 먼저 실행되도록 우선순위를
|
||||
명시한 것은 실제 취소가 성공한 뒤에야 활동 로그를 남기기 위함입니다 — 순서가 뒤바뀌면
|
||||
"로그는 있는데 취소는 실패"가 생깁니다. `RegisterEasyPayMethodsListener`가 8종의 간편결제
|
||||
방식을 하나의 필터 훅에서 한 번에 주입하는 이유는 §settings.md 의 설정 스키마와 대칭을
|
||||
맞추기 위함입니다 — 설정 키가 8개인데 훅 등록이 여러 곳으로 흩어지면 한쪽만 갱신되는
|
||||
사각이 생깁니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 훅 리스너
|
||||
|
||||
<!-- @generated:listeners START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 리스너 | 구독 훅 | 등록 방식 | HookListenerInterface | 파일 |
|
||||
|---|---|---|---|---|
|
||||
| `AdjustEcommercePaymentMethodsLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/AdjustEcommercePaymentMethodsLayoutListener.php` |
|
||||
| `CancelActivityLogListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/CancelActivityLogListener.php` |
|
||||
| `EnsureAdminOrderDetailPaymentQueryLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/EnsureAdminOrderDetailPaymentQueryLayoutListener.php` |
|
||||
| `EnsureAdminOrderListTestBadgeLayoutListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/EnsureAdminOrderListTestBadgeLayoutListener.php` |
|
||||
| `PaymentRefundListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/PaymentRefundListener.php` |
|
||||
| `RegisterEasyPayMethodsListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/RegisterEasyPayMethodsListener.php` |
|
||||
| `RegisterPgProviderListener` | 2개 | 명시 등록 | ✅ | `src/Listeners/RegisterPgProviderListener.php` |
|
||||
| `RestoreLayoutExtensionsAfterUpdateListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/RestoreLayoutExtensionsAfterUpdateListener.php` |
|
||||
<!-- @generated:listeners END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`RestoreLayoutExtensionsAfterUpdateListener`는 `plugin:update`가 레이아웃 확장 조각(§레이아웃
|
||||
확장)의 활성/비활성 상태를 초기화할 수 있어서 존재합니다 — 운영자가 특정 화면을 꺼둔 상태로
|
||||
업데이트해도 그 선택이 사라지지 않도록 복원합니다. 8개 리스너 전부가
|
||||
`HookListenerInterface`를 구현하는 것은 이 저장소의 전 리스너 공통 계약입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 레이아웃 확장
|
||||
|
||||
<!-- @generated:layout-extensions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 대상 | 설명 |
|
||||
|---|---|
|
||||
| `resources/extensions/admin_order_list_test_badge.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/admin_order_payment_query.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/checkout_easy_pay.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/user_mypage_order_receipt.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/user_order_complete_receipt.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
<!-- @generated:layout-extensions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
5개 조각은 각각 독립적인 화면 관심사입니다 — 관리자 주문 목록의 테스트배지, 관리자 주문
|
||||
상세의 거래조회 UI, 체크아웃의 간편결제 버튼, 마이페이지·주문완료의 영수증 버튼이 서로
|
||||
다른 화면에 주입되므로 하나로 합치지 않았습니다. `user_mypage_order_receipt.json`과
|
||||
`user_order_complete_receipt.json`이 별도 파일인 것은 두 화면의 컴포넌트 트리와 데이터
|
||||
소스가 다르기 때문입니다(§AGENTS.md "설계 원칙" — 상태는 이커머스 소유이므로 화면마다
|
||||
필요한 조회 방식이 다를 수 있습니다).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 미들웨어
|
||||
|
||||
<!-- @generated:middleware START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 미들웨어 | 부착 대상(targets) | 우선순위 |
|
||||
|---|---|---|
|
||||
| `VbankNotifyIpWhitelist` | `web.plugins.sirsoft-pay_nicepayments.payment.vbank-notify` | - |
|
||||
<!-- @generated:middleware END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 결과 콜백(`/payment/callback`)에는 이 미들웨어가 붙지 않습니다 — 그 경로는 브라우저가
|
||||
POST 하는 경로라 발신 IP 가 사용자마다 다르기 때문입니다. IP 화이트리스트가 의미 있는 것은
|
||||
나이스페이먼츠 서버가 직접 호출하는 가상계좌 입금통보뿐입니다. 로컬/테스트 환경에서는
|
||||
이 제한을 자동 우회합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 브로드캐스트 채널
|
||||
|
||||
<!-- @generated:channels START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 브로드캐스트 채널이 없습니다._
|
||||
<!-- @generated:channels END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 승인·통보는 전부 동기 HTTP 요청/응답 안에서 끝나는 흐름이라 실시간 브로드캐스트가
|
||||
필요한 지점이 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 스케줄
|
||||
|
||||
<!-- @generated:schedules START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 스케줄이 없습니다._
|
||||
<!-- @generated:schedules END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
가상계좌 만료는 이 플러그인이 크론으로 스캔하지 않고, 만료 이후 도착하는 나이스페이먼츠
|
||||
입금통보를 거부하는 방식으로 처리됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 알림 정의
|
||||
|
||||
<!-- @generated:notifications START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 알림 정의가 없습니다._
|
||||
<!-- @generated:notifications END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 완료/실패 알림은 이커머스 모듈이 주문 상태 변화를 기준으로 발송하는 공용 알림에 이미
|
||||
포함됩니다 — PG 마다 별도 알림 정의를 만들면 같은 이벤트에 대해 PG 수만큼 중복 정의가
|
||||
생깁니다. 운영자에게 실패를 알리고 싶은 확장은 §발행 훅의 `refund_failed` 를 구독합니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,79 @@
|
||||
# 나이스페이먼츠 — 프론트엔드
|
||||
|
||||
> 레이아웃·액션 핸들러·전역 진입점·에셋 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 레이아웃
|
||||
|
||||
<!-- @generated:layouts START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
레이아웃 1개 (루트: `resources/layouts`).
|
||||
|
||||
| 그룹 | 개수 |
|
||||
|---|---|
|
||||
| `admin` | 1개 |
|
||||
|
||||
| 레이아웃 | 그룹 | 종류 | extends |
|
||||
|---|---|---|---|
|
||||
| `plugin_settings` | `admin` | 화면 | `_admin_base` |
|
||||
<!-- @generated:layouts END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
다른 PG 플러그인들과 마찬가지로 이 플러그인이 소유한 화면 레이아웃은 관리자 설정 화면
|
||||
하나뿐입니다 — 체크아웃·주문상세·마이페이지의 결제 UI는 이 플러그인 소유가 아니라
|
||||
§레이아웃 확장(다른 확장/템플릿 레이아웃에 주입되는 조각)으로 존재합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 액션 핸들러
|
||||
|
||||
<!-- @generated:handlers START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
핸들러 2개 (정의: `resources/js/handlers/index.ts`).
|
||||
|
||||
| 핸들러 | 레이아웃에서 부르는 이름 |
|
||||
|---|---|
|
||||
| `requestPayment` | `sirsoft-pay_nicepayments.requestPayment` |
|
||||
| `setPaymentMethod` | `sirsoft-pay_nicepayments.setPaymentMethod` |
|
||||
<!-- @generated:handlers END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`setPaymentMethod`가 별도로 필요한 이유는 나이스페이먼츠 간편결제 버튼(네이버페이/카카오페이/
|
||||
삼성페이/애플페이/PAYCO/11pay/SSG페이/L.pay 8종)이 레이아웃 컴포넌트가 아니라 PG가
|
||||
제공하는 DOM을 그대로 쓰기 때문입니다 — React 상태로 선택 하이라이트를 그리는 대신 DOM을
|
||||
직접 조작해 선택된 버튼에 테두리를 입힙니다(`updateEasyPayButtonStyles`). `requestPayment`
|
||||
하나로 PC/모바일 2가지 프로토콜(§docs/architecture.md "인증→승인 2단계")을 모두 처리합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 전역 진입점
|
||||
|
||||
<!-- @generated:frontend-entry START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 엔트리 파일 | `resources/js/index.ts` |
|
||||
| 전역 객체 | `window.__SirsoftNicepayments` |
|
||||
| 재등록 진입점 | `initPlugin()` |
|
||||
|
||||
로케일 전환 시 코어가 이 진입점을 호출해 핸들러를 다시 등록합니다. 진입점은 핸들러 재등록만 수행하고 1회성 부팅 작업을 포함하지 않습니다.
|
||||
<!-- @generated:frontend-entry END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`window.__SirsoftNicepayments`로 노출되는 이유는 코어가 로케일 전환 시 이 이름으로 재등록
|
||||
진입점을 찾기 때문입니다(§CLAUDE.md "재등록 진입점"). 나이스페이먼츠 결제창 스크립트
|
||||
자체는 이 진입점이 미리 로드하지 않습니다 — `requestPayment` 핸들러가 결제 시도 시점에
|
||||
동적으로 스크립트를 삽입한 뒤 `goPay()`를 호출합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 에셋
|
||||
|
||||
<!-- @generated:assets START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 구분 |
|
||||
|---|---|
|
||||
| `dist/js/plugin.iife.js` | 빌드 산출물 (커밋 대상) |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 (manifest) |
|
||||
|
||||
로딩 설정: `{"strategy":"global","priority":100,"dependencies":[]}`
|
||||
<!-- @generated:assets END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
나이스페이먼츠가 제공하는 결제창 SDK는 이 목록에 없습니다 — `requestPayment` 핸들러가
|
||||
결제 시도 시점에 동적으로 로드하는 제3자 자산이라, 이 플러그인이 빌드 시 번들링하는
|
||||
`dist/` 산출물과는 다른 층입니다. CSS 산출물이 없는 것은 결제창 자체는 PG 가 그리고, 이
|
||||
플러그인은 간편결제 버튼 같은 최소한의 UI만 코어 컴포넌트로 구성하기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,102 @@
|
||||
# 나이스페이먼츠 — 설정·권한·라우트
|
||||
|
||||
> 설정 스키마·권한·메뉴·라우트·의존 관계 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설정 스키마
|
||||
|
||||
<!-- @generated:settings-schema START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 타입 | 기본값 | 설명 |
|
||||
|---|---|---|---|
|
||||
| `is_test_mode` | `boolean` | `true` | 테스트 모드 |
|
||||
| `test_mid` | `string` | `nicepay00m` | 테스트 가맹점 ID (MID) |
|
||||
| `test_merchant_key` | `string` | `EYzu8jGGMfqaDEp76gSckuvnaHHu+bC4opsSN6lHv3b2lurNYkVXrZ7Z1AoqQnXI3eLuaUFyoRNC6FkrzVjceg==` | 테스트 가맹점 키 |
|
||||
| `live_mid` | `string` | - | 라이브 가맹점 ID (MID) |
|
||||
| `live_merchant_key` | `string` | - | 라이브 가맹점 키 |
|
||||
| `redirect_success_url` | `string` | `{shopBase}/orders/{orderId}/complete` | 결제 성공 리다이렉트 URL |
|
||||
| `redirect_fail_url` | `string` | `{shopBase}/checkout` | 결제 실패 리다이렉트 URL |
|
||||
| `use_escrow` | `boolean` | `false` | 에스크로 결제 사용 |
|
||||
| `easy_pay_allow_with_other_pg` | `boolean` | `false` | 타 PG와 사용가능함 |
|
||||
| `easy_pay_naverpay` | `boolean` | `false` | 네이버페이 간편결제 |
|
||||
| `easy_pay_kakaopay` | `boolean` | `false` | 카카오페이 간편결제 |
|
||||
| `easy_pay_samsungpay` | `boolean` | `false` | 삼성페이 간편결제 |
|
||||
| `easy_pay_applepay` | `boolean` | `false` | 애플페이 간편결제 |
|
||||
| `easy_pay_payco` | `boolean` | `false` | PAYCO 간편결제 |
|
||||
| `easy_pay_skpay` | `boolean` | `false` | 11pay (SK페이) 간편결제 |
|
||||
| `easy_pay_ssgpay` | `boolean` | `false` | SSG페이 간편결제 |
|
||||
| `easy_pay_lpay` | `boolean` | `false` | L.pay 간편결제 |
|
||||
|
||||
기본값 파일: `config/settings/defaults.json` · 설정 화면 레이아웃: `resources/layouts/admin/plugin_settings.json`
|
||||
<!-- @generated:settings-schema END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`test_*`/`live_*` 쌍 구조는 다른 PG 플러그인과 동일한 이유입니다 — 테스트 모드와 운영
|
||||
모드가 완전히 다른 자격증명을 쓰므로 `is_test_mode`를 켜고 꺼도 서로의 값을 덮어쓰지
|
||||
않습니다. 간편결제 플래그가 8개(네이버페이/카카오페이/삼성페이/애플페이/PAYCO/11pay/
|
||||
SSG페이/L.pay)로 다른 PG 플러그인보다 많은 것은 나이스페이먼츠가 실제로 이 8종 전부를
|
||||
중계하기 때문입니다 — 계약이 없는 수단을 켜면 결제 시도 시점에 PG 오류로 드러납니다
|
||||
(§AGENTS.md "금지 패턴"과 유사하게, 계약 여부는 이 플러그인이 검증하지 않고 PG 응답에
|
||||
위임합니다).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 권한
|
||||
|
||||
<!-- @generated:permissions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_선언된 권한이 없습니다._
|
||||
<!-- @generated:permissions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 설정 접근 권한은 이커머스의 관리자 권한 체계 안에서 다뤄집니다 — PG 마다 별도 권한을
|
||||
선언하면 PG 를 여러 개 설치했을 때 "결제 설정을 볼 수 있는 사람"이라는 하나의 개념이
|
||||
플러그인 수만큼 중복 정의됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 메뉴
|
||||
|
||||
<!-- @generated:menus START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 메뉴가 없습니다._
|
||||
<!-- @generated:menus END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
설정 화면(`plugin_settings.json`)은 코어의 "플러그인 관리 > 설정" 공통 진입점을 통해
|
||||
접근합니다 — PG 플러그인마다 전용 사이드바 메뉴를 만들면 PG 를 여러 개 설치했을 때 메뉴가
|
||||
난립합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 라우트
|
||||
|
||||
<!-- @generated:routes START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 파일 | URL prefix |
|
||||
|---|---|---|
|
||||
| `api` | `src/routes/api.php` | `/api/plugins/sirsoft-pay_nicepayments/...` |
|
||||
| `web` | `src/routes/web.php` | `/plugins/sirsoft-pay_nicepayments/...` |
|
||||
|
||||
확장 라우트는 **활성 상태인 확장의 것만** 등록됩니다. 라우트 정의를 바꾸면 라우트 캐시 재생성이 필요합니다.
|
||||
<!-- @generated:routes END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`api`(Bearer 토큰 인증, 관리자 거래조회·에스크로 배송등록처럼 로그인 사용자가 직접
|
||||
호출하는 엔드포인트)와 `web`(결제 콜백·가상계좌 입금통보처럼 결제창이나 PG 서버가
|
||||
도달하는 엔드포인트)이 분리된 이유는 인증 방식이 다르기 때문입니다 — 나이스페이먼츠는
|
||||
우리 서비스의 Bearer 토큰을 모르므로 콜백 라우트에 `api` 인증 미들웨어를 걸 수 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 의존 관계
|
||||
|
||||
<!-- @generated:dependencies START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
| 확장 | 유형 | 버전 제약 | 번들 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-ecommerce` | 모듈 | `>=1.1.0` | ✅ |
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
없음.
|
||||
<!-- @generated:dependencies END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`sirsoft-ecommerce >=1.1.0` 하드 의존은 §data-model.md 에서 설명한 구조(결제 상태는
|
||||
이커머스가 소유, 이 플러그인은 절차만 소유)의 직접적 결과입니다 — 이커머스 없이는 이
|
||||
플러그인이 다룰 주문 자체가 존재하지 않습니다. 이커머스의 PG 등록 훅이나 `Order` 모델
|
||||
구조가 바뀌면 이 최소 버전을 올려야 합니다(§CLAUDE.md "확장 → 확장 동기화").
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,178 @@
|
||||
# 토스페이먼츠 — 에이전트 가이드
|
||||
|
||||
> 이 문서는 이 플러그인을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 [README.md](README.md) 를 보세요.
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 유형: 플러그인 (sirsoft-tosspayments) — 토스페이먼츠 PG 연동(통합결제창 SDK/가상계좌 웹훅 secret 대조/에스크로 3-상태). 소유 테이블 없음 — 상태는 sirsoft-ecommerce 소유
|
||||
2. 확장 방식: `RegisterPgProviderListener`/`RegisterTossPaymentMethodsListener`/`RegisterCashReceiptProviderListener` 로 이커머스 레지스트리에 등록 — 이커머스 코드는 이 플러그인을 모른다
|
||||
3. 건드리면 안 되는 것: 결제 승인 확인 시 서버가 재계산한 금액과 PG 콜백 금액 대조(amount mismatch 검사) 생략, 가상계좌 웹훅의 secret 대조(`webhook_secret_verify`) 우회 — 토스는 notify IP 목록·서명을 제공하지 않아 secret 대조가 유일한 위조 방지 수단
|
||||
4. 작업 위치: `plugins/_bundled/sirsoft-tosspayments` — 활성 디렉토리 직접 수정 금지
|
||||
5. 반영: `php artisan plugin:update sirsoft-tosspayments --force`
|
||||
```
|
||||
|
||||
## 1. 이 확장은 무엇인가
|
||||
|
||||
<!-- @intent START -->
|
||||
토스페이먼츠 PG(결제 게이트웨이)를 `sirsoft-ecommerce`에 연결하는 어댑터입니다. 승인은
|
||||
브라우저 리다이렉트 기반입니다 — 통합결제창 SDK(`js.tosspayments.com/v2/standard`)가
|
||||
결제를 처리한 뒤 브라우저를 `?paymentKey&orderId&amount`가 붙은 콜백 URL로 리다이렉트하고,
|
||||
서버가 그 파라미터로 승인 확인 API를 호출합니다. 다른 PG 플러그인(iframe 팝업/CLI/SOAP)과
|
||||
달리 프론트엔드 계층이 SDK 호출 하나로 끝나고 프로토콜 복잡도가 대부분 서버 쪽(확인·웹훅)에
|
||||
있습니다.
|
||||
|
||||
이 플러그인은 **결제창형**과 **주문서형**(`order_sheet_mode`) 두 UI 모드를 지원합니다.
|
||||
결제창형은 통합결제창이 결제수단을 전부 처리하는 카드 하나로 노출되고, 주문서형은
|
||||
`method_*` 설정으로 활성화한 개별 토스 결제수단(카드/가상계좌/계좌이체/휴대폰/토스페이/
|
||||
카카오페이/네이버페이/페이코/삼성페이)이 체크아웃 화면에 개별 버튼으로 뜹니다 — 어느
|
||||
쪽이든 최종 처리는 토스 결제창 하나로 귀결되므로 각 수단은 `pg_provider` 를 이 플러그인
|
||||
자신으로 고정(`pg_locked`)합니다(§AGENTS.md "4. 확장점").
|
||||
|
||||
**설계 원칙**: 이 플러그인도 상태를 소유하지 않습니다(§data-model.md — 모델·테이블 0개).
|
||||
가상계좌 웹훅 검증은 토스가 notify IP 목록이나 서명을 제공하지 않는다는 제약에서
|
||||
비롯됩니다 — 승인 확인 응답에만 실리는 `secret` 값을 `payment_meta`에 저장해 두었다가
|
||||
웹훅이 도착하면 대조하는 것이 토스 공식 문서가 제시하는 유일한 위조 방지 수단입니다.
|
||||
|
||||
**의도적으로 하지 않는 것**: 승인 확인 시 PG가 돌려준 금액과 서버가 재계산한 주문 금액이
|
||||
다르면(`amount_mismatch`) 결제를 완료 처리하지 않고 실패로 되돌립니다 — 콜백 URL의
|
||||
`amount` 쿼리 파라미터는 브라우저를 거치므로 신뢰할 수 없는 입력이기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 2. 디렉토리 지도
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-tosspayments --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-tosspayments --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-tosspayments --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-tosspayments --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
|
||||
## 3. 핵심 흐름
|
||||
|
||||
<!-- @intent START -->
|
||||
**결제 승인**: 통합결제창이 브라우저를 `/payment/callback?paymentKey&orderId&amount`로
|
||||
리다이렉트 → `PaymentCallbackController`가 주문 조회 →
|
||||
`sirsoft-tosspayments.payment.before_confirm` 훅 → `TossPaymentsApiService::confirmPayment()`
|
||||
가 토스 승인 확인 API 호출 → 응답 금액과 서버 재계산 금액 대조(불일치 시 실패 처리) →
|
||||
`sirsoft-tosspayments.payment.after_confirm` 훅 → 이커머스 주문 결제 완료 처리. 가상계좌가
|
||||
발급된 경우 응답에 실린 `secret`을 `payment_meta.toss_secret`에 저장해 이후 웹훅 대조에
|
||||
씁니다.
|
||||
|
||||
**가상계좌 입금 웹훅**: 토스가 `/webhook/deposit`으로 POST → 저장된 `toss_secret`과 웹훅
|
||||
본문의 `secret`을 대조(`webhook_secret_verify` 설정이 꺼져 있지 않은 한 강제) → 일치하면
|
||||
결제 완료 처리, 불일치하면 경고 로그만 남기고 처리하지 않습니다.
|
||||
|
||||
**결제 취소(환불)**: 관리자가 주문 취소(`cancel_pg=true`) → 코어가
|
||||
`sirsoft-ecommerce.payment.refund` 필터 발화 → `PaymentRefundListener`가 토스 취소 API 호출
|
||||
→ `before_cancel`/`after_cancel` 훅 발화.
|
||||
|
||||
**설정 저장 검증**: 관리자가 플러그인 설정을 저장 → `core.plugin_settings.before_save`
|
||||
(동기 훅, `sync: true`) → `ValidateTossSettingsListener`가 `vbank_valid_hours`(1~2160시간)와
|
||||
`use_escrow`(`off`/`on`/`buyer_choice` 3-상태) 범위를 검증 → 위반 시 `ValidationException`으로
|
||||
저장 자체를 막습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 4. 확장점
|
||||
|
||||
<!-- @generated:extension-points-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 확장점 | 수 | 상세 |
|
||||
|---|---|---|
|
||||
| 발행 훅 | 4개 | [발행 훅](docs/extension-points.md#발행-훅) |
|
||||
| 구독 훅 | 9개 | [구독 훅](docs/extension-points.md#구독-훅) |
|
||||
| 훅 리스너 | 6개 | [훅 리스너](docs/extension-points.md#훅-리스너) |
|
||||
| 레이아웃 확장 | 3개 | [레이아웃 확장](docs/extension-points.md#레이아웃-확장) |
|
||||
| 미들웨어 | 0개 | [미들웨어](docs/extension-points.md#미들웨어) |
|
||||
| 브로드캐스트 채널 | 0개 | [브로드캐스트 채널](docs/extension-points.md#브로드캐스트-채널) |
|
||||
| 스케줄 | 0개 | [스케줄](docs/extension-points.md#스케줄) |
|
||||
| 알림 정의 | 0개 | [알림 정의](docs/extension-points.md#알림-정의) |
|
||||
<!-- @generated:extension-points-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`before_confirm`/`before_cancel`은 API 호출 **전** 개입 지점이라 예외를 던지면 실제
|
||||
토스 호출이 일어나지 않습니다. `core.plugin_settings.before_save`를 `ValidateTossSettingsListener`
|
||||
가 구독하는 것은 다른 PG 플러그인에는 없는 패턴입니다 — 이 훅은 코어 `PluginSettingsService`
|
||||
가 발행하며, `sync: true`가 없으면 큐로 비동기 디스패치되어 `ValidationException`이 워커
|
||||
안에서 죽고 저장은 그대로 진행됩니다(§CLAUDE.md "Listener 데이터 접근 규정" 의 sync 훅
|
||||
규칙과 동일한 이유).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 5. 수정 시 동반 의무
|
||||
|
||||
- [ ] `_bundled` 에서만 수정하고 `php artisan plugin:update sirsoft-tosspayments --force` 로 반영
|
||||
- [ ] manifest version 상향 시 `package.json` · `package-lock.json` · `composer.json` 동기화 + CHANGELOG 기재
|
||||
- [ ] 발행 훅 추가·이름 변경 시 `php artisan ext:docgen` 재실행 (구독하는 확장의 계약이 바뀝니다)
|
||||
- [ ] API 표면 변경 시 `php artisan api:docgen --scope=plugin:sirsoft-tosspayments` 재실행 + `docs/api/**` 갱신
|
||||
- [ ] 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
|
||||
- [ ] TSX/TS 변경 시 `--production` 재빌드 후 `dist/` 커밋 (sourceMappingURL 잔존 금지)
|
||||
- [ ] 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
|
||||
- [ ] 승인 확인에서 금액 대조(`amount_mismatch`) 로직을 우회하거나 완화하지 않는다
|
||||
- [ ] 가상계좌 웹훅의 secret 대조(`webhook_secret_verify`)를 기본값 `true` 이외로 바꾸지 않는다 — 끄면 토스 노티 위조를 막을 수단이 사라진다
|
||||
- [ ] `order_sheet_mode` 관련 로직을 고칠 때 `RegisterPgProviderListener`(enabled_methods)와 `RegisterTossPaymentMethodsListener`(builtin 결제수단 주입) 양쪽을 함께 갱신 — 한쪽만 고치면 설정과 노출 목록이 어긋난다
|
||||
- [ ] `ValidateTossSettingsListener`에 새 범위 검증을 추가하면 `core.plugin_settings.before_save` 의 `sync: true`를 유지
|
||||
|
||||
## 6. 금지 패턴
|
||||
|
||||
<!-- @intent START -->
|
||||
| 금지 | 올바른 사용 | 이유 |
|
||||
|---|---|---|
|
||||
| 콜백 URL의 `amount` 쿼리 파라미터를 그대로 신뢰해 결제 완료 처리 | 서버가 주문 금액을 재계산해 PG 응답 금액과 대조, 불일치 시 실패 처리 | 콜백은 브라우저를 거치므로 사용자가 쿼리 파라미터를 조작해 실제 결제 금액보다 낮은 금액으로 완료 처리를 유도할 수 있다 |
|
||||
| 가상계좌 웹훅의 secret 대조를 생략하거나 항상 통과 | `payment_meta.toss_secret`과 웹훅 본문의 secret을 항상 대조 | 토스는 notify IP 목록·서명을 제공하지 않아 secret 대조가 유일한 위조 방지 수단이다 — 생략하면 제3자가 임의 주문에 대해 위조 입금통보를 보낼 수 있다 |
|
||||
| `core.plugin_settings.before_save` 리스너에 `sync: true` 없이 등록 | 저장을 막아야 하는 검증 훅은 반드시 `sync: true` | 기본값(비동기 큐)이면 `ValidationException`이 워커 안에서 죽고 저장이 그대로 진행되어 검증이 무력화된다 |
|
||||
| 라이브 시크릿 키를 로그·에러 메시지에 노출 | 운영 키는 항상 마스킹하거나 로그 대상에서 제외 | 노출되면 제3자가 서버측 API를 위조 호출할 수 있다 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 7. 테스트 실행
|
||||
|
||||
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 개수 | 위치 |
|
||||
|---|---|---|
|
||||
| PHPUnit | 12개 | `plugins/_bundled/sirsoft-tosspayments/tests` |
|
||||
| Vitest | 5개 | `vitest.config.ts` |
|
||||
| Playwright | 0개 | — |
|
||||
| 시나리오 매니페스트 | 3개 | `tests/scenarios` |
|
||||
|
||||
기저 TestCase: `tests/PluginTestCase.php` — 확장 테스트는 이 클래스를 상속합니다 (`Tests\TestCase` 직접 상속 금지).
|
||||
|
||||
```bash
|
||||
# PHPUnit (변경 범위만) (Bash)
|
||||
php vendor/bin/phpunit plugins/_bundled/sirsoft-tosspayments/tests --filter='<대상클래스>'
|
||||
|
||||
# Vitest (확장 디렉토리에서) (PowerShell)
|
||||
cd plugins/_bundled/sirsoft-tosspayments && powershell -Command "npm run test:run -- <대상>"
|
||||
|
||||
```
|
||||
|
||||
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
|
||||
<!-- @generated:test-commands END -->
|
||||
|
||||
## 8. 문서 목차
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
@@ -0,0 +1,213 @@
|
||||
# 토스페이먼츠
|
||||
|
||||
**G7 플러그인 · sirsoft-tosspayments**
|
||||
토스페이먼츠 결제 게이트웨이 (통합결제창 연동)
|
||||
|
||||
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-1.0.2-0066FF?style=flat-square" alt="version 1.0.2">
|
||||
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
|
||||
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 >=7.0.10">
|
||||
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
|
||||
<img src="https://img.shields.io/badge/requires-sirsoft--ecommerce-BF8700?style=flat-square" alt="requires sirsoft-ecommerce">
|
||||
</p>
|
||||
<!-- @generated:badges END -->
|
||||
|
||||
---
|
||||
|
||||
[소개](#소개) · [주요 기능](#주요-기능) · [동작 방식](#동작-방식) · [요구 사항](#요구-사항) · [설치](#설치) · [관리자 설정](#관리자-설정) · [사용 방법](#사용-방법) · [다른 확장과의 연동](#다른-확장과의-연동) · [문서](#문서) · [트러블슈팅](#트러블슈팅) · [변경 이력](#변경-이력) · [라이선스](#라이선스)
|
||||
|
||||
---
|
||||
|
||||
## 소개
|
||||
|
||||
<!-- @intent START -->
|
||||
토스페이먼츠 통합결제창 결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다.
|
||||
승인은 브라우저 리다이렉트 기반입니다 — 결제창(SDK)이 결제를 처리한 뒤 브라우저를 콜백
|
||||
URL로 돌려보내고, 서버가 그 파라미터로 승인 확인 API를 호출합니다.
|
||||
|
||||
이 플러그인은 결제창형(통합결제창 카드 하나)과 주문서형(개별 토스 결제수단 버튼) 두 UI
|
||||
모드를 지원합니다. 결제 자체의 상태(주문·결제 성공/실패/취소)는 소유하지 않습니다 — 그
|
||||
상태는 `sirsoft-ecommerce`의 주문·결제 테이블에 있고, 이 플러그인은 "그 상태를
|
||||
토스페이먼츠 API 와 어떻게 주고받는가"만 책임집니다. 그래서 이 플러그인은 소유
|
||||
테이블/모델이 하나도 없습니다(§data-model.md).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 주요 기능
|
||||
|
||||
<!-- @intent START -->
|
||||
| 영역 | 설명 |
|
||||
|---|---|
|
||||
| 승인 방식 | 통합결제창 SDK + 브라우저 리다이렉트 콜백 + 서버 승인 확인 |
|
||||
| UI 모드 | 결제창형(단일 카드) / 주문서형(개별 결제수단 버튼) 전환 |
|
||||
| 결제수단 | 카드, 가상계좌, 계좌이체, 휴대폰, 토스페이, 카카오페이, 네이버페이, 페이코, 삼성페이 |
|
||||
| 가상계좌 | 발급 + 웹훅 입금통보(secret 대조로 위조 방지) |
|
||||
| 에스크로 | 3-상태(끔/켬/구매자선택) |
|
||||
| 현금영수증 | 카드/계좌이체 발급·취소 프로바이더 등록 |
|
||||
| 결제 취소 | 전액/부분취소, 실패 시 별도 훅 |
|
||||
| 설정 저장 검증 | 가상계좌 유효시간·에스크로 값 서버측 범위 강제 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 동작 방식
|
||||
|
||||
<!-- @intent START -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[체크아웃 주문 생성] --> B["통합결제창 SDK 호출"]
|
||||
B --> C["/payment/callback (paymentKey·orderId·amount)"]
|
||||
C --> D[서버가 금액 재계산 후 대조]
|
||||
D -->|일치| E["승인 확인 API 호출"]
|
||||
D -->|불일치| F[결제 실패 처리]
|
||||
E --> G[주문 결제 완료 처리]
|
||||
G --> H[성공 URL 리다이렉트]
|
||||
```
|
||||
|
||||
가상계좌가 발급되면 승인 확인 응답에만 실리는 secret 값을 저장해 두었다가, 토스가 입금
|
||||
웹훅을 보내면 그 secret 을 대조해 위조를 막습니다 — 토스는 notify IP 목록이나 서명을
|
||||
제공하지 않아 이 방식이 공식적으로 제시되는 유일한 검증 수단입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 요구 사항
|
||||
|
||||
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| G7 코어 | `>=7.0.10` |
|
||||
| PHP | `^8.2` |
|
||||
| 의존 모듈 | `sirsoft-ecommerce` `>=1.1.0` |
|
||||
<!-- @generated:requirements END -->
|
||||
|
||||
## 설치
|
||||
|
||||
<!-- @generated:install START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
```bash
|
||||
# 번들 설치 (코어에 동봉된 소스에서 설치)
|
||||
php artisan plugin:install sirsoft-tosspayments
|
||||
|
||||
# 활성화
|
||||
php artisan plugin:activate sirsoft-tosspayments
|
||||
|
||||
# 업데이트 (번들 소스 기준 강제 반영)
|
||||
php artisan plugin:update sirsoft-tosspayments --force
|
||||
```
|
||||
|
||||
저장소: https://github.com/gnuboard/g7-plugin-sirsoft-tosspayments
|
||||
<!-- @generated:install END -->
|
||||
|
||||
## 관리자 설정
|
||||
|
||||
<!-- @generated:settings-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 의미 | 기본값 |
|
||||
|---|---|---|
|
||||
| `is_test_mode` | 테스트 모드 | `true` |
|
||||
| `test_client_key` | 테스트 클라이언트 키 | - |
|
||||
| `test_secret_key` | 테스트 시크릿 키 | - |
|
||||
| `live_client_key` | 라이브 클라이언트 키 | - |
|
||||
| `live_secret_key` | 라이브 시크릿 키 | - |
|
||||
| `redirect_success_url` | 결제 성공 리다이렉트 URL | `{shopBase}/orders/{orderId}/complete` |
|
||||
| `redirect_fail_url` | 결제 실패 리다이렉트 URL | `{shopBase}/checkout` |
|
||||
| `order_sheet_mode` | 주문서형 결제 | `false` |
|
||||
| `method_card` | 카드 | `true` |
|
||||
| `method_virtual_account` | 가상계좌 | `false` |
|
||||
| `method_transfer` | 계좌이체 | `false` |
|
||||
| `method_mobile_phone` | 휴대폰 | `false` |
|
||||
| `method_tosspay` | 토스페이 | `false` |
|
||||
| `method_kakaopay` | 카카오페이 | `false` |
|
||||
| `method_naverpay` | 네이버페이 | `false` |
|
||||
| `method_payco` | 페이코 | `false` |
|
||||
| `method_samsungpay` | 삼성페이 | `false` |
|
||||
| `vbank_valid_hours` | 가상계좌 입금기한(시간) | `24` |
|
||||
| `vbank_cash_receipt_type` | 가상계좌 현금영수증 유형 | - |
|
||||
| `use_escrow` | 에스크로 사용 | `off` |
|
||||
| `webhook_secret_verify` | 웹훅 secret 검증 | `true` |
|
||||
|
||||
개발자용 상세(타입·검증·저장 위치)는 [설정 스키마](docs/settings.md#설정-스키마) 를 보세요.
|
||||
<!-- @generated:settings-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`order_sheet_mode`를 켜야 `method_*` 개별 결제수단 플래그가 체크아웃 화면에 실제로
|
||||
반영됩니다 — 꺼둔 상태(기본값)에서는 `method_*` 값을 바꿔도 통합결제창이 결제수단을
|
||||
전부 처리하므로 화면에 변화가 없습니다. 라이브 키(클라이언트 키·시크릿 키)는 외부에
|
||||
노출하지 마세요.
|
||||
|
||||
**웹훅 URL 등록** — 토스페이먼츠 개발자센터에 아래 URL을 실제 운영 도메인으로 등록합니다.
|
||||
|
||||
```text
|
||||
https://your-domain.com/plugins/sirsoft-tosspayments/webhook/deposit
|
||||
```
|
||||
|
||||
`webhook_secret_verify`(기본값 켜짐)를 끄면 이 웹훅의 위조 방지 수단이 사라지므로 특별한
|
||||
이유가 없는 한 켜둡니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 사용 방법
|
||||
|
||||
<!-- @intent START -->
|
||||
**결제창형으로 시작하기**: 별도 설정 없이 활성화만 하면 통합결제창 카드 하나로 카드·계좌이체·
|
||||
가상계좌·휴대폰결제가 전부 처리됩니다. 간편결제(토스페이/카카오페이/네이버페이 등)를
|
||||
개별 버튼으로 노출하고 싶다면 `order_sheet_mode`를 켜고 해당 `method_*` 플래그를
|
||||
활성화하세요.
|
||||
|
||||
**결제 취소/부분취소**: 관리자가 주문 취소를 요청(`cancel_pg=true`)하면 코어가
|
||||
`sirsoft-ecommerce.payment.refund` 필터 훅을 발화하고, `PaymentRefundListener`가
|
||||
토스페이먼츠 취소 API를 호출합니다. `after_cancel`은 취소 API 가 **성공했을 때만**
|
||||
발화합니다 — 실패하면 `TossPaymentsApiException`이 던져지고 코어가 이를 받아 취소 요청
|
||||
자체를 실패로 응답합니다(이 플러그인에는 나이스페이먼츠의 `refund_failed` 같은 별도
|
||||
실패 훅이 없습니다).
|
||||
|
||||
**가상계좌 확인**: 테스트 모드에서 가상계좌 웹훅이 도착하지 않으면 §웹훅 URL 등록이
|
||||
완료됐는지, `webhook_secret_verify`가 켜져 있다면 저장된 secret 이 정상 발급됐는지
|
||||
확인합니다.
|
||||
|
||||
전체 API 목록은 [docs/api/](docs/api/README.md) 를, 발행/구독 훅 목록은
|
||||
[docs/extension-points.md](docs/extension-points.md) 를 참고하세요.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 다른 확장과의 연동
|
||||
|
||||
<!-- @generated:integrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
| 확장 | 유형 | 버전 제약 | 번들 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-ecommerce` | 모듈 | `>=1.1.0` | ✅ |
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
없음.
|
||||
<!-- @generated:integrations END -->
|
||||
|
||||
## 문서
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
<!-- @intent START -->
|
||||
| 증상 | 원인 | 조치 |
|
||||
|---|---|---|
|
||||
| 결제는 성공했는데 체크아웃으로 실패 리다이렉트됨 | 콜백 `amount`가 서버 재계산 금액과 불일치 | 의도된 안전장치 — 쿠폰/재고 변경 등으로 주문 금액이 결제 시점과 달라졌는지 확인 |
|
||||
| 가상계좌 입금통보가 반영되지 않음 | 웹훅 URL 미등록, 또는 secret 불일치로 조용히 무시됨 | §웹훅 URL 등록 확인 + 로그의 `deposit webhook secret mismatch` 경고 확인 |
|
||||
| 주문서형으로 켰는데 개별 결제수단 버튼이 안 보임 | `order_sheet_mode`는 켰지만 해당 `method_*` 플래그 비활성 | 노출하려는 결제수단의 `method_*`를 개별로 활성화 |
|
||||
| 설정 저장 시 422 오류 | `vbank_valid_hours` 범위(1~2160) 또는 `use_escrow` 값(`off`/`on`/`buyer_choice`) 위반 | `ValidateTossSettingsListener`가 의도적으로 차단한 것 — 값을 허용 범위로 수정 |
|
||||
| 결제 성공했는데 간편결제 버튼 클릭 시 오류 | 토스페이먼츠 계약이 없는 결제수단을 활성화 | 계약이 완료된 결제수단만 관리자 설정에서 활성화 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 변경 이력
|
||||
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,22 @@
|
||||
# 토스페이먼츠 개발자 문서
|
||||
|
||||
> plugins/_bundled/sirsoft-tosspayments · 플러그인
|
||||
|
||||
<!-- @generated:stats START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**훅 수**: 4 · **구독 훅 수**: 9 · **라우트 수**: 4 · **모델 수**: 0 · **테이블 수**: 0 · **마이그레이션 수**: 0 · **레이아웃 수**: 1 · **핸들러 수**: 1
|
||||
<!-- @generated:stats END -->
|
||||
|
||||
## 문서 목차
|
||||
|
||||
<!-- @generated:doc-toc START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | 설계 의도·계층 지도·디렉토리 맵 |
|
||||
| [extension-points.md](extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 |
|
||||
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
|
||||
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
|
||||
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
|
||||
| [api/](api/README.md) | API 레퍼런스 |
|
||||
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
|
||||
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
|
||||
<!-- @generated:doc-toc END -->
|
||||
@@ -0,0 +1,58 @@
|
||||
# 토스페이먼츠 — 아키텍처
|
||||
|
||||
> 설계 의도와 계층 구조 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설계 의도
|
||||
|
||||
<!-- @intent START -->
|
||||
토스페이먼츠 통합결제창을 `sirsoft-ecommerce`에 연결하는 어댑터입니다. 다른 PG
|
||||
플러그인(iframe 팝업, CLI 실행, SOAP)과 달리 이 플러그인은 순수 리다이렉트 기반입니다 —
|
||||
결제창이 브라우저를 콜백 URL로 돌려보내고, 서버는 그 쿼리 파라미터로 승인을 확인합니다.
|
||||
이 단순함의 대가로 콜백 파라미터(특히 `amount`)를 신뢰하지 않고 서버가 재검증해야
|
||||
합니다(§AGENTS.md "의도적으로 하지 않는 것").
|
||||
|
||||
가상계좌 웹훅 검증도 다른 PG 와 다릅니다 — IP 화이트리스트가 아니라 결제 승인 응답에만
|
||||
실리는 secret 값 대조입니다. 토스가 notify IP 목록이나 서명을 제공하지 않기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 계층 지도
|
||||
|
||||
<!-- @intent START -->
|
||||
```text
|
||||
Controller (PaymentCallbackController / WebhookController)
|
||||
→ TossPaymentsApiService (승인 확인 · 취소 API 호출)
|
||||
→ sirsoft-ecommerce 의 Order/OrderPayment 모델 (직접 참조 — 이 플러그인 소유 모델 없음)
|
||||
|
||||
Listener (RegisterPgProviderListener / RegisterTossPaymentMethodsListener / ValidateTossSettingsListener 등)
|
||||
→ sirsoft-ecommerce 의 필터 훅 + 코어 설정 저장 훅에 등록 (컴파일 타임 결합 없음)
|
||||
```
|
||||
|
||||
`ValidateTossSettingsListener`가 `core.plugin_settings.before_save`(코어 설정 저장 훅)를
|
||||
구독하는 것은 이 플러그인만의 계층 특징입니다 — 결제 도메인 훅(`sirsoft-ecommerce.payment.*`)
|
||||
뿐 아니라 코어 설정 저장 경로에도 개입해 저장 시점에 값을 검증합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 디렉토리
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-tosspayments --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-tosspayments --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-tosspayments --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-tosspayments --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
@@ -0,0 +1,64 @@
|
||||
# 토스페이먼츠 — 데이터 모델
|
||||
|
||||
> 모델·소유 테이블·마이그레이션·Enum · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 모델
|
||||
|
||||
<!-- @generated:models START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_소유 모델이 없습니다._
|
||||
<!-- @generated:models END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 상태는 이 플러그인이 아니라 `sirsoft-ecommerce`의 `Order`/`OrderPayment` 모델이
|
||||
소유합니다(§AGENTS.md "설계 원칙"). 가상계좌 웹훅 검증에 쓰는 secret 조차 별도 테이블이
|
||||
아니라 `OrderPayment.payment_meta`(JSON 컬럼)의 `toss_secret` 키에 저장됩니다 — 이 값은
|
||||
그 주문 하나에만 의미가 있어 독립 테이블을 둘 이유가 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 소유 테이블
|
||||
|
||||
<!-- @generated:tables START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_소유 테이블이 없습니다._
|
||||
<!-- @generated:tables END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
가상계좌 발급 정보와 토스 거래키(`paymentKey`)는 이커머스 `OrderPayment` 테이블의 기존
|
||||
컬럼/메타에 저장됩니다 — PG 마다 별도 결제상세 테이블을 두면 관리자 주문 상세가 PG
|
||||
종류에 따라 다른 테이블을 조인해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 마이그레이션
|
||||
|
||||
<!-- @generated:migrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_마이그레이션이 없습니다._
|
||||
<!-- @generated:migrations END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
소유 테이블이 없으므로(§소유 테이블) 스키마 변경 자체가 발생하지 않습니다. 설정 스키마
|
||||
변경(§settings.md)은 `config/settings/defaults.json` 갱신만으로 끝나며 DB 마이그레이션
|
||||
대상이 아닙니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Enum
|
||||
|
||||
<!-- @generated:enums START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_Enum 이 없습니다._
|
||||
<!-- @generated:enums END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`use_escrow`의 3-상태(`off`/`on`/`buyer_choice`)는 Enum이 아니라
|
||||
`ValidateTossSettingsListener::USE_ESCROW_VALUES` 상수 배열로 검증합니다 — 설정값 하나에만
|
||||
쓰이는 닫힌 어휘라 Enum 승격의 이득(여러 곳에서 타입으로 재사용)이 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Repository
|
||||
|
||||
<!-- @generated:repositories START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_Repository 가 없습니다._
|
||||
<!-- @generated:repositories END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
이 플러그인이 이커머스 `Order`/`OrderPayment`를 읽고 쓰는 지점(컨트롤러·리스너)은 모두
|
||||
이커머스가 이미 노출한 Eloquent 모델을 직접 참조합니다 — 자기 소유 테이블이 없는 상태에서
|
||||
남의 모델을 감싸는 Repository 를 새로 만드는 것은 위임만 하는 빈 계층입니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,135 @@
|
||||
# 토스페이먼츠 — 확장점
|
||||
|
||||
> 발행/구독 훅·미들웨어·채널·스케줄 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 발행 훅
|
||||
|
||||
<!-- @generated:hooks-published START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
발행 훅 4종 / 호출 지점 4곳.
|
||||
|
||||
| 훅 이름 | 유형 | 설명 | 발행 위치 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-tosspayments.payment.after_cancel` | action | 토스페이먼츠 결제 취소 완료 후 | `src/Services/TossPaymentsApiService.php:102` |
|
||||
| `sirsoft-tosspayments.payment.after_confirm` | action | 토스페이먼츠 결제 승인 완료 후 | `src/Controllers/PaymentCallbackController.php:98` |
|
||||
| `sirsoft-tosspayments.payment.before_cancel` | action | 토스페이먼츠 결제 취소 API 호출 전 | `src/Services/TossPaymentsApiService.php:98` |
|
||||
| `sirsoft-tosspayments.payment.before_confirm` | action | 토스페이먼츠 결제 승인 API 호출 전 | `src/Controllers/PaymentCallbackController.php:94` |
|
||||
<!-- @generated:hooks-published END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`before_confirm`/`before_cancel`은 API 호출 **전** 개입 지점이라 예외를 던지면 실제 토스
|
||||
호출이 일어나지 않습니다. `after_confirm`은 승인 확인 응답을 받은 뒤(§AGENTS.md "핵심 흐름")
|
||||
발화하므로, 이 시점에는 아직 금액 대조가 끝나지 않았을 수 있습니다 — 구독하는 확장은
|
||||
이커머스가 최종 완료 처리를 마쳤는지 별도로 확인해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 구독 훅
|
||||
|
||||
<!-- @generated:hooks-subscribed START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 훅 이름 | 유형 | 리스너 | 메서드 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| `core.plugin_settings.before_save` | action (미선언) | `ValidateTossSettingsListener` | `validateBeforeSave` | 10 |
|
||||
| `core.plugins.updated` | action | `RestoreLayoutExtensionsAfterUpdateListener` | `restoreCurrentExtensionsAfterUpdate` | 20 |
|
||||
| `sirsoft-ecommerce.cash_receipt.cancel` | filter | `RegisterCashReceiptProviderListener` | `cancel` | 10 |
|
||||
| `sirsoft-ecommerce.cash_receipt.issue` | filter | `RegisterCashReceiptProviderListener` | `issue` | 10 |
|
||||
| `sirsoft-ecommerce.cash_receipt.registered_providers` | filter | `RegisterCashReceiptProviderListener` | `registerProvider` | 10 |
|
||||
| `sirsoft-ecommerce.payment.get_client_config` | filter | `RegisterPgProviderListener` | `getClientConfig` | 10 |
|
||||
| `sirsoft-ecommerce.payment.refund` | filter | `PaymentRefundListener` | `processRefund` | 10 |
|
||||
| `sirsoft-ecommerce.payment.registered_pg_providers` | filter | `RegisterPgProviderListener` | `registerProvider` | 10 |
|
||||
| `sirsoft-ecommerce.settings.filter_available_payment_methods` | filter | `RegisterTossPaymentMethodsListener` | `injectTossMethods` | 20 |
|
||||
<!-- @generated:hooks-subscribed END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`core.plugin_settings.before_save`는 다른 PG 플러그인에는 없는 구독입니다 — 이 플러그인만
|
||||
설정 저장 시점에 서버측 범위 검증(`vbank_valid_hours`, `use_escrow`)을 강제합니다
|
||||
(§AGENTS.md "핵심 흐름"). `RegisterTossPaymentMethodsListener`가 `order_sheet_mode`가
|
||||
꺼져 있으면 아무것도 주입하지 않는 것은 결제창형에서는 토스 결제수단 전부가 통합결제창
|
||||
카드 하나로 처리되기 때문입니다 — 개별 버튼을 만들 필요가 없습니다. 3종의 현금영수증 훅
|
||||
(`cash_receipt.*`)은 카드/계좌이체 발급, 취소, 프로바이더 등록을 각각 담당하며 모두
|
||||
`RegisterCashReceiptProviderListener` 하나가 처리합니다 — PG 선택과 현금영수증 발급사
|
||||
선택이 이커머스에서 독립적인 개념이라 별도 등록입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 훅 리스너
|
||||
|
||||
<!-- @generated:listeners START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 리스너 | 구독 훅 | 등록 방식 | HookListenerInterface | 파일 |
|
||||
|---|---|---|---|---|
|
||||
| `PaymentRefundListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/PaymentRefundListener.php` |
|
||||
| `RegisterCashReceiptProviderListener` | 3개 | 명시 등록 | ✅ | `src/Listeners/RegisterCashReceiptProviderListener.php` |
|
||||
| `RegisterPgProviderListener` | 2개 | 명시 등록 | ✅ | `src/Listeners/RegisterPgProviderListener.php` |
|
||||
| `RegisterTossPaymentMethodsListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/RegisterTossPaymentMethodsListener.php` |
|
||||
| `RestoreLayoutExtensionsAfterUpdateListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/RestoreLayoutExtensionsAfterUpdateListener.php` |
|
||||
| `ValidateTossSettingsListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/ValidateTossSettingsListener.php` |
|
||||
<!-- @generated:listeners END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`ValidateTossSettingsListener`는 구독 훅이 `sirsoft-ecommerce.*`가 아니라
|
||||
`core.plugin_settings.before_save`라는 점에서 이 표의 다른 5개 리스너와 성격이 다릅니다 —
|
||||
결제 도메인이 아니라 코어 설정 저장 파이프라인에 개입합니다. `getSubscribedHooks()`에서
|
||||
`sync: true`를 선언하는 것도 이 리스너뿐입니다(§AGENTS.md "금지 패턴").
|
||||
<!-- @intent END -->
|
||||
|
||||
## 레이아웃 확장
|
||||
|
||||
<!-- @generated:layout-extensions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 대상 | 설명 |
|
||||
|---|---|
|
||||
| `resources/extensions/admin_order_payment.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/checkout-payment-error.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/user_order_show.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
<!-- @generated:layout-extensions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`checkout-payment-error.json`은 다른 PG 플러그인에는 없는 조각입니다 — 승인 확인 실패
|
||||
(`amount_mismatch`, 서명 오류 등)가 `?error=` 쿼리로 체크아웃에 되돌아왔을 때 그 오류를
|
||||
사용자에게 보여주는 전용 UI입니다. 다른 PG는 오류 안내를 체크아웃 레이아웃이 이미 가진
|
||||
공용 오류 처리에 맡기지만, 이 플러그인은 리다이렉트 기반 승인이라 오류 사유가 다양해
|
||||
전용 조각을 둡니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 미들웨어
|
||||
|
||||
<!-- @generated:middleware START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 미들웨어가 없습니다._
|
||||
<!-- @generated:middleware END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
다른 PG 플러그인은 가상계좌 입금통보에 IP 화이트리스트 미들웨어를 부착하지만, 이
|
||||
플러그인은 미들웨어가 없습니다 — 토스가 notify IP 목록을 제공하지 않아 IP 기반 검증
|
||||
자체가 불가능하기 때문입니다. 대신 §핵심 흐름의 secret 대조(`WebhookController` 안의
|
||||
애플리케이션 레벨 검증)가 그 역할을 대신합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 브로드캐스트 채널
|
||||
|
||||
<!-- @generated:channels START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 브로드캐스트 채널이 없습니다._
|
||||
<!-- @generated:channels END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 승인·웹훅은 전부 동기 HTTP 요청/응답 안에서 끝나는 흐름이라 실시간 브로드캐스트가
|
||||
필요한 지점이 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 스케줄
|
||||
|
||||
<!-- @generated:schedules START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 스케줄이 없습니다._
|
||||
<!-- @generated:schedules END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
가상계좌 만료는 이 플러그인이 크론으로 스캔하지 않고, 만료 이후 도착하는 토스 입금 웹훅을
|
||||
거부하는 방식으로 처리됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 알림 정의
|
||||
|
||||
<!-- @generated:notifications START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 알림 정의가 없습니다._
|
||||
<!-- @generated:notifications END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 완료/실패 알림은 이커머스 모듈이 주문 상태 변화를 기준으로 발송하는 공용 알림에 이미
|
||||
포함됩니다 — PG 마다 별도 알림 정의를 만들면 같은 이벤트에 대해 PG 수만큼 중복 정의가
|
||||
생깁니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,79 @@
|
||||
# 토스페이먼츠 — 프론트엔드
|
||||
|
||||
> 레이아웃·액션 핸들러·전역 진입점·에셋 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 레이아웃
|
||||
|
||||
<!-- @generated:layouts START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
레이아웃 1개 (루트: `resources/layouts`).
|
||||
|
||||
| 그룹 | 개수 |
|
||||
|---|---|
|
||||
| `admin` | 1개 |
|
||||
|
||||
| 레이아웃 | 그룹 | 종류 | extends |
|
||||
|---|---|---|---|
|
||||
| `plugin_settings` | `admin` | 화면 | `_admin_base` |
|
||||
<!-- @generated:layouts END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
다른 PG 플러그인들과 마찬가지로 이 플러그인이 소유한 화면 레이아웃은 관리자 설정 화면
|
||||
하나뿐입니다 — 체크아웃·주문상세의 결제 UI는 이 플러그인 소유가 아니라 §레이아웃 확장
|
||||
(다른 확장/템플릿 레이아웃에 주입되는 조각)으로 존재합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 액션 핸들러
|
||||
|
||||
<!-- @generated:handlers START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
핸들러 1개 (정의: `resources/js/handlers/index.ts`).
|
||||
|
||||
| 핸들러 | 레이아웃에서 부르는 이름 |
|
||||
|---|---|
|
||||
| `requestPayment` | `sirsoft-tosspayments.requestPayment` |
|
||||
<!-- @generated:handlers END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
핸들러가 이것 하나뿐인 이유는 이 플러그인이 통합결제창 SDK 호출 이후를 전부 브라우저
|
||||
리다이렉트에 위임하기 때문입니다(§AGENTS.md "1. 이 확장은 무엇인가") — 다른 PG처럼 결제
|
||||
수단 선택 UI를 DOM으로 직접 조작할 필요가 없습니다. `order_sheet_mode`에 따라 통합결제창
|
||||
하나(카드 한 장)를 열지, 사용자가 고른 개별 토스 결제수단(`params.paymentMethodId`)을
|
||||
지정해 열지가 갈리지만(`params.paymentMethod`, 미지정 시 `_local.paymentMethod` 참조) 그
|
||||
분기도 이 핸들러 하나 안에서 처리합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 전역 진입점
|
||||
|
||||
<!-- @generated:frontend-entry START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 엔트리 파일 | `resources/js/index.ts` |
|
||||
| 전역 객체 | `window.__SirsoftTosspayments` |
|
||||
| 재등록 진입점 | `initPlugin()` |
|
||||
|
||||
로케일 전환 시 코어가 이 진입점을 호출해 핸들러를 다시 등록합니다. 진입점은 핸들러 재등록만 수행하고 1회성 부팅 작업을 포함하지 않습니다.
|
||||
<!-- @generated:frontend-entry END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`window.__SirsoftTosspayments`로 노출되는 이유는 코어가 로케일 전환 시 이 이름으로 재등록
|
||||
진입점을 찾기 때문입니다(§CLAUDE.md "재등록 진입점"). 토스 SDK(`js.tosspayments.com/v2/standard`)
|
||||
자체는 이 진입점이 미리 로드하지 않습니다 — `requestPayment` 핸들러가 결제 시도 시점에
|
||||
동적으로 로드합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 에셋
|
||||
|
||||
<!-- @generated:assets START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 구분 |
|
||||
|---|---|
|
||||
| `dist/js/plugin.iife.js` | 빌드 산출물 (커밋 대상) |
|
||||
|
||||
로딩 설정: `{"strategy":"global","priority":100,"dependencies":[]}`
|
||||
<!-- @generated:assets END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
토스 SDK 자체는 이 목록에 없습니다 — `requestPayment` 핸들러가 결제 시도 시점에 동적으로
|
||||
로드하는 제3자 자산이라, 이 플러그인이 빌드 시 번들링하는 `dist/` 산출물과는 다른 층입니다.
|
||||
다른 PG 플러그인과 달리 `editor-spec.json`이 없는 것은 이 플러그인이 레이아웃 편집기에서
|
||||
커스터마이즈 가능한 전용 컴포넌트를 노출하지 않기 때문입니다 — 결제 UI는 코어 기본
|
||||
컴포넌트와 §레이아웃 확장 조각만으로 구성됩니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,105 @@
|
||||
# 토스페이먼츠 — 설정·권한·라우트
|
||||
|
||||
> 설정 스키마·권한·메뉴·라우트·의존 관계 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설정 스키마
|
||||
|
||||
<!-- @generated:settings-schema START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 타입 | 기본값 | 설명 |
|
||||
|---|---|---|---|
|
||||
| `is_test_mode` | `boolean` | `true` | 테스트 모드 |
|
||||
| `test_client_key` | `string` | - | 테스트 클라이언트 키 |
|
||||
| `test_secret_key` | `string` | - | 테스트 시크릿 키 |
|
||||
| `live_client_key` | `string` | - | 라이브 클라이언트 키 |
|
||||
| `live_secret_key` | `string` | - | 라이브 시크릿 키 |
|
||||
| `redirect_success_url` | `string` | `{shopBase}/orders/{orderId}/complete` | 결제 성공 리다이렉트 URL |
|
||||
| `redirect_fail_url` | `string` | `{shopBase}/checkout` | 결제 실패 리다이렉트 URL |
|
||||
| `order_sheet_mode` | `boolean` | `false` | 주문서형 결제 |
|
||||
| `method_card` | `boolean` | `true` | 카드 |
|
||||
| `method_virtual_account` | `boolean` | `false` | 가상계좌 |
|
||||
| `method_transfer` | `boolean` | `false` | 계좌이체 |
|
||||
| `method_mobile_phone` | `boolean` | `false` | 휴대폰 |
|
||||
| `method_tosspay` | `boolean` | `false` | 토스페이 |
|
||||
| `method_kakaopay` | `boolean` | `false` | 카카오페이 |
|
||||
| `method_naverpay` | `boolean` | `false` | 네이버페이 |
|
||||
| `method_payco` | `boolean` | `false` | 페이코 |
|
||||
| `method_samsungpay` | `boolean` | `false` | 삼성페이 |
|
||||
| `vbank_valid_hours` | `integer` | `24` | 가상계좌 입금기한(시간) |
|
||||
| `vbank_cash_receipt_type` | `string` | - | 가상계좌 현금영수증 유형 |
|
||||
| `use_escrow` | `string` | `off` | 에스크로 사용 |
|
||||
| `webhook_secret_verify` | `boolean` | `true` | 웹훅 secret 검증 |
|
||||
|
||||
기본값 파일: `config/settings/defaults.json` · 설정 화면 레이아웃: `resources/layouts/admin/plugin_settings.json`
|
||||
<!-- @generated:settings-schema END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`order_sheet_mode`가 이 스키마의 분기점입니다 — `false`(결제창형)면 `method_*` 8개 플래그는
|
||||
읽히지 않고 통합결제창이 결제수단 선택을 전담합니다. `true`(주문서형)로 켜야 `method_*`
|
||||
플래그가 실제로 체크아웃 화면의 개별 버튼 노출 여부를 결정합니다(§AGENTS.md "이 확장은
|
||||
무엇인가"). `vbank_valid_hours`(1~2160시간)와 `use_escrow`(`off`/`on`/`buyer_choice`)는
|
||||
`ValidateTossSettingsListener`가 저장 시점에 범위를 강제합니다 — UI 의 input 힌트만으로는
|
||||
관리자 설정 저장 API 직접 호출을 막을 수 없기 때문입니다. `webhook_secret_verify`를 끄면
|
||||
가상계좌 웹훅의 유일한 위조 방지 수단이 사라지므로 기본값 `true`를 유지해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 권한
|
||||
|
||||
<!-- @generated:permissions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_선언된 권한이 없습니다._
|
||||
<!-- @generated:permissions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 설정 접근 권한은 이커머스의 관리자 권한 체계 안에서 다뤄집니다 — PG 마다 별도 권한을
|
||||
선언하면 PG 를 여러 개 설치했을 때 "결제 설정을 볼 수 있는 사람"이라는 하나의 개념이
|
||||
플러그인 수만큼 중복 정의됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 메뉴
|
||||
|
||||
<!-- @generated:menus START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 메뉴가 없습니다._
|
||||
<!-- @generated:menus END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
설정 화면(`plugin_settings.json`)은 코어의 "플러그인 관리 > 설정" 공통 진입점을 통해
|
||||
접근합니다 — PG 플러그인마다 전용 사이드바 메뉴를 만들면 PG 를 여러 개 설치했을 때 메뉴가
|
||||
난립합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 라우트
|
||||
|
||||
<!-- @generated:routes START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 파일 | URL prefix |
|
||||
|---|---|---|
|
||||
| `web` | `src/routes/web.php` | `/plugins/sirsoft-tosspayments/...` |
|
||||
|
||||
확장 라우트는 **활성 상태인 확장의 것만** 등록됩니다. 라우트 정의를 바꾸면 라우트 캐시 재생성이 필요합니다.
|
||||
<!-- @generated:routes END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
다른 PG 플러그인과 달리 `api` 라우트 파일이 없습니다 — 이 플러그인은 로그인 사용자가
|
||||
Bearer 토큰으로 직접 호출하는 엔드포인트(예: 관리자 거래조회)를 두지 않습니다. 승인
|
||||
콜백·가상계좌 웹훅 모두 결제창 리다이렉트나 토스 서버가 도달하는 경로라 `web`에만
|
||||
있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 의존 관계
|
||||
|
||||
<!-- @generated:dependencies START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
| 확장 | 유형 | 버전 제약 | 번들 |
|
||||
|---|---|---|---|
|
||||
| `sirsoft-ecommerce` | 모듈 | `>=1.1.0` | ✅ |
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
없음.
|
||||
<!-- @generated:dependencies END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`sirsoft-ecommerce >=1.1.0` 하드 의존은 §data-model.md 에서 설명한 구조(결제 상태는
|
||||
이커머스가 소유, 이 플러그인은 절차만 소유)의 직접적 결과입니다 — 이커머스 없이는 이
|
||||
플러그인이 다룰 주문 자체가 존재하지 않습니다. 이커머스의 PG 등록 훅이나 `Order` 모델
|
||||
구조가 바뀌면 이 최소 버전을 올려야 합니다(§CLAUDE.md "확장 → 확장 동기화").
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,187 @@
|
||||
# KG이니시스 본인인증 — 에이전트 가이드
|
||||
|
||||
> 이 문서는 이 플러그인을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 [README.md](README.md) 를 보세요.
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 유형: 플러그인 (sirsoft-verification_kginicis) — KG이니시스 본인확인(reqSvcCd=03) IDV Provider. 코어 `IdentityVerificationInterface` 12메서드 구현, PII 레코드 소유 (payment 플러그인과 달리 소유 테이블 있음)
|
||||
2. 확장 방식: `RegisterInicisProviderListener` 로 코어 `core.identity.registered_providers` 필터에 등록 — 코어는 이 플러그인의 존재를 모른다
|
||||
3. 건드리면 안 되는 것: 비로그인 사용자 PII 캐시 stash(`inicis:pending_record:` 접두) 로직 우회, 라이브 MID `SRB` 프리픽스 정책값 상수(`LIVE_MID_PREFIX`) 미참조, 중복가입 차단(`AssertNoDuplicateInicisIdentity`) 우회
|
||||
4. 작업 위치: `plugins/_bundled/sirsoft-verification_kginicis` — 활성 디렉토리 직접 수정 금지
|
||||
5. 반영: `php artisan plugin:update sirsoft-verification_kginicis --force`
|
||||
```
|
||||
|
||||
## 1. 이 확장은 무엇인가
|
||||
|
||||
<!-- @intent START -->
|
||||
KG이니시스 본인확인(휴대폰 인증, `reqSvcCd=03`)을 코어 본인인증(IDV) 체계에 연결하는
|
||||
Provider 입니다. 코어 `IdentityVerificationInterface`(표준 12메서드)를 구현해, 회원가입·
|
||||
비밀번호 찾기·민감작업 등 코어가 정의한 모든 IDV 강제 지점에서 이메일 인증 대신 이니시스
|
||||
팝업이 대신 동작하게 합니다. `sirsoft-verification_nhnkcp`도 같은 인터페이스를 구현하며,
|
||||
운영자는 둘 중 어느 것이든(또는 둘 다) 설치해 사용할 수 있습니다 — 코어는 등록된
|
||||
provider ID로만 구분하고 어느 PG사인지 모릅니다.
|
||||
|
||||
**결제 PG 플러그인과의 결정적 차이**: 이 플러그인은 실제 PII(개인식별정보) 레코드를
|
||||
소유합니다(§data-model.md — `inicis_identity_records` 테이블, `InicisIdentityRecord`
|
||||
모델). 결제 플러그인들이 "상태는 남의 것, 절차만 내 것"이었던 것과 달리, 본인확인은 그
|
||||
확인 결과(이름·생년월일·성별·CI/DI 등)를 이 플러그인이 직접 보관해야 이후 재확인 없이
|
||||
"본인확인 완료 여부"를 판단할 수 있습니다.
|
||||
|
||||
**의도적으로 하지 않는 것**: 비로그인 사용자(예: 회원가입 도중)의 PII는 확인 즉시
|
||||
DB 에 쓰지 않고 Cache 에 임시 저장(`inicis:pending_record:` 접두)했다가, 가입이 실제로
|
||||
완료된 뒤(`core.auth.after_register` 훅)에야 레코드로 흡수합니다 — 가입을 완료하지 않은
|
||||
방문자의 PII 를 DB 에 영구 저장하지 않기 위함입니다. 사용자가 탈퇴하거나 계정이 삭제되면
|
||||
`core.user.after_withdraw`/`core.user.before_delete` 훅에서 관련 레코드를 정리합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 2. 디렉토리 지도
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Http/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Http/Resources/` | API 리소스 | 목록 응답은 화면이 실제로 그리는 것만 싣는다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Repositories/` | 데이터 접근 | 목록 쿼리는 컬럼 프루닝·정렬 화이트리스트 확인 |
|
||||
| `src/Models/` | Eloquent 모델 | 스키마 변경 시 마이그레이션 + 업그레이드 스텝 동반 |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/Enums/` | 상태·타입·분류 | 문자열 리터럴 대신 Enum 을 SSoT 로 둔다 |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `database/migrations/` | 마이그레이션 | 한국어 comment + `down()` 필수, 기설치본은 업그레이드 스텝으로 백필 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-verification_kginicis --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-verification_kginicis --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-verification_kginicis --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-verification_kginicis --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-verification_kginicis --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
|
||||
## 3. 핵심 흐름
|
||||
|
||||
<!-- @intent START -->
|
||||
**본인확인 시작~완료**: 코어가 IDV 를 요구하는 지점(회원가입 등)에서 428 응답 →
|
||||
프론트 `startAuth` 핸들러가 사용자 클릭 컨텍스트 안에서 `window.open`으로 빈 팝업 생성
|
||||
(Chrome popup blocker 회피 — 자동 호출은 차단되지만 클릭 직후 호출은 통과) → 코어
|
||||
challenge 시작 응답의 `mid`/`mtxid`/`authHash`로 팝업에 이니시스 인증 폼 제출 → 이니시스
|
||||
인증 완료 후 `InicisChallengeMappingRepository`가 mTxId ↔ challenge_id 매핑을 저장 →
|
||||
인증 결과는 postMessage 또는 팝업 종료 감지로 회수 → `InicisIdentityProvider::verify()`가
|
||||
SEED 복호화 후 결과를 반환. 성인인증(`inicis.adult_verification` purpose)으로 발행된
|
||||
challenge 는 만 19세 이상만 통과시킵니다.
|
||||
|
||||
**비로그인 사용자(회원가입 도중) 처리**: `verify()` 시점에 로그인 사용자가 없으면 PII 를
|
||||
Cache 에 stash(`inicis:pending_record:{key}`) → 회원가입 완료 → `core.auth.after_register`
|
||||
훅 → `CompleteInicisRecordAfterRegister`가 같은 캐시 키로 PII 를 회수해
|
||||
`InicisIdentityRecord`로 흡수.
|
||||
|
||||
**중복가입 차단**: `core.auth.before_register` 훅 → `AssertNoDuplicateInicisIdentity`가
|
||||
`duplicate_block_enabled` 설정이 켜져 있으면 `duplicate_field`(DI 또는 CI) 기준으로
|
||||
`InicisIdentityLogQueryRepository`를 조회해 이미 가입된 동일인이 있는지 확인 → 있으면
|
||||
가입을 차단.
|
||||
|
||||
**사용자 삭제/탈퇴 시 PII 정리**: `core.user.before_delete`/`core.user.after_withdraw` 훅 →
|
||||
`CleanInicisRecordOnUserDelete`/`CleanInicisRecordOnUserWithdraw`가 해당 사용자의
|
||||
`inicis_identity_records`를 정리.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 4. 확장점
|
||||
|
||||
<!-- @generated:extension-points-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 확장점 | 수 | 상세 |
|
||||
|---|---|---|
|
||||
| 발행 훅 | 3개 | [발행 훅](docs/extension-points.md#발행-훅) |
|
||||
| 구독 훅 | 6개 | [구독 훅](docs/extension-points.md#구독-훅) |
|
||||
| 훅 리스너 | 6개 | [훅 리스너](docs/extension-points.md#훅-리스너) |
|
||||
| 레이아웃 확장 | 2개 | [레이아웃 확장](docs/extension-points.md#레이아웃-확장) |
|
||||
| 미들웨어 | 0개 | [미들웨어](docs/extension-points.md#미들웨어) |
|
||||
| 브로드캐스트 채널 | 0개 | [브로드캐스트 채널](docs/extension-points.md#브로드캐스트-채널) |
|
||||
| 스케줄 | 0개 | [스케줄](docs/extension-points.md#스케줄) |
|
||||
| 알림 정의 | 0개 | [알림 정의](docs/extension-points.md#알림-정의) |
|
||||
<!-- @generated:extension-points-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`core.identity.registered_providers` 는 코어가 등록된 IDV provider 목록을 모으는 필터
|
||||
훅입니다 — 새 IDV PG 를 추가하려는 확장은 이 훅에 자기 provider 를 등록하면 됩니다
|
||||
(`sirsoft-verification_nhnkcp`가 동일 패턴). `core.plugin_settings.update_validation_rules`는
|
||||
`ValidateInicisSettingsListener`가 `is_test_mode=false`(라이브 모드) 진입 시
|
||||
`live_mid`/`live_api_key`에 `required` 규칙을 동적으로 부여하는 자리입니다 — 코어
|
||||
`UpdatePluginSettingsRequest`의 정적 스키마는 "테스트 모드일 땐 선택, 라이브 모드일 땐
|
||||
필수" 같은 조건부 검증을 표현할 수 없기 때문입니다. `live_mid`의 `SRB` 프리픽스는 이
|
||||
필터가 아니라 `InicisIdentityProvider::buildLiveMid()`가 그 값을 실제로 쓸 때(요청 조립
|
||||
시점) 동적으로 부착하므로 별도 형식 검증을 두지 않습니다 — DB 에는 운영자가 입력한 원본
|
||||
값이 그대로 저장됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 5. 수정 시 동반 의무
|
||||
|
||||
- [ ] `_bundled` 에서만 수정하고 `php artisan plugin:update sirsoft-verification_kginicis --force` 로 반영
|
||||
- [ ] manifest version 상향 시 `package.json` · `package-lock.json` · `composer.json` 동기화 + CHANGELOG 기재
|
||||
- [ ] 스키마 변경 시 마이그레이션(한국어 comment + `down()`) + 기설치본 백필용 업그레이드 스텝
|
||||
- [ ] 발행 훅 추가·이름 변경 시 `php artisan ext:docgen` 재실행 (구독하는 확장의 계약이 바뀝니다)
|
||||
- [ ] API 표면 변경 시 `php artisan api:docgen --scope=plugin:sirsoft-verification_kginicis` 재실행 + `docs/api/**` 갱신
|
||||
- [ ] 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
|
||||
- [ ] TSX/TS 변경 시 `--production` 재빌드 후 `dist/` 커밋 (sourceMappingURL 잔존 금지)
|
||||
- [ ] 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
|
||||
- [ ] PII 컬럼(이름·생년월일·성별·CI/DI 등)을 다루는 코드 변경 시 GDPR 삭제/탈퇴 정리 리스너(`CleanInicisRecordOnUserDelete`/`CleanInicisRecordOnUserWithdraw`)가 여전히 그 컬럼을 정리하는지 확인
|
||||
- [ ] 팝업 기반 인증 흐름(`startAuth`)을 고칠 때 `window.open`을 사용자 클릭 컨텍스트 밖으로 옮기지 않는다 — Chrome popup blocker 회피가 깨진다
|
||||
- [ ] `duplicate_field`/`duplicate_block_enabled` 로직을 고치면 `InicisDuplicateField` Enum 과 `AssertNoDuplicateInicisIdentity`를 함께 갱신
|
||||
|
||||
## 6. 금지 패턴
|
||||
|
||||
<!-- @intent START -->
|
||||
| 금지 | 올바른 사용 | 이유 |
|
||||
|---|---|---|
|
||||
| 비로그인 사용자의 PII 를 verify 즉시 DB 에 저장 | Cache 에 stash(`inicis:pending_record:` 접두) 후 가입 완료 시 흡수 | 가입을 완료하지 않은 방문자의 PII 를 DB 에 영구 저장하면 불필요한 개인정보 보유가 된다 |
|
||||
| 사용자 삭제/탈퇴 리스너 없이 PII 컬럼 추가 | `CleanInicisRecordOnUserDelete`/`CleanInicisRecordOnUserWithdraw`에 정리 로직 동반 | 정리 누락 시 탈퇴한 사용자의 PII 가 무기한 남는다 |
|
||||
| `LIVE_MID_PREFIX` 상수를 참조하지 않고 `'SRB'`를 문자열로 재작성 | `InicisIdentityProvider::LIVE_MID_PREFIX` 참조 | 이니시스 프리픽스 정책이 바뀌면 상수 1곳만 갱신해야 런타임 로직 전체에 반영된다 — 문자열 재작성은 사각을 만든다 |
|
||||
| 팝업을 사용자 클릭 이벤트 핸들러 밖(비동기 콜백 등)에서 `window.open` | 사용자 제스처 컨텍스트 안에서 직접 호출 | Chrome 등 브라우저는 사용자 제스처 없이 열리는 팝업을 자동 차단한다 |
|
||||
| 라이브 API 키를 로그·에러 메시지에 노출 | 운영 키는 항상 마스킹하거나 로그 대상에서 제외 | 노출되면 제3자가 본인확인 API 를 위조 호출할 수 있다 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 7. 테스트 실행
|
||||
|
||||
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 개수 | 위치 |
|
||||
|---|---|---|
|
||||
| PHPUnit | 22개 | `plugins/_bundled/sirsoft-verification_kginicis/tests` |
|
||||
| Vitest | 8개 | `vitest.config.ts` |
|
||||
| Playwright | 0개 | — |
|
||||
| 시나리오 매니페스트 | 8개 | `tests/scenarios` |
|
||||
|
||||
기저 TestCase: `tests/PluginTestCase.php` — 확장 테스트는 이 클래스를 상속합니다 (`Tests\TestCase` 직접 상속 금지).
|
||||
|
||||
```bash
|
||||
# PHPUnit (변경 범위만) (Bash)
|
||||
php vendor/bin/phpunit plugins/_bundled/sirsoft-verification_kginicis/tests --filter='<대상클래스>'
|
||||
|
||||
# Vitest (확장 디렉토리에서) (PowerShell)
|
||||
cd plugins/_bundled/sirsoft-verification_kginicis && powershell -Command "npm run test:run -- <대상>"
|
||||
|
||||
```
|
||||
|
||||
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
|
||||
<!-- @generated:test-commands END -->
|
||||
|
||||
## 8. 문서 목차
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
@@ -0,0 +1,182 @@
|
||||
# KG이니시스 본인인증
|
||||
|
||||
**G7 플러그인 · sirsoft-verification_kginicis**
|
||||
KG이니시스 통합인증의 본인확인(reqSvcCd=03)을 G7 코어 IDV 인프라에 Provider 로 등록하는 플러그인
|
||||
|
||||
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-1.0.4-0066FF?style=flat-square" alt="version 1.0.4">
|
||||
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
|
||||
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.8-1F883D?style=flat-square" alt="G7 >=7.0.8">
|
||||
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
|
||||
</p>
|
||||
<!-- @generated:badges END -->
|
||||
|
||||
---
|
||||
|
||||
[소개](#소개) · [주요 기능](#주요-기능) · [동작 방식](#동작-방식) · [요구 사항](#요구-사항) · [설치](#설치) · [관리자 설정](#관리자-설정) · [사용 방법](#사용-방법) · [다른 확장과의 연동](#다른-확장과의-연동) · [문서](#문서) · [트러블슈팅](#트러블슈팅) · [변경 이력](#변경-이력) · [라이선스](#라이선스)
|
||||
|
||||
---
|
||||
|
||||
## 소개
|
||||
|
||||
<!-- @intent START -->
|
||||
KG이니시스 본인확인(휴대폰 인증, reqSvcCd=03)을 G7 코어의 본인인증(IDV) 체계에 연결하는
|
||||
플러그인입니다. 코어가 정의한 표준 인터페이스를 구현해, 회원가입·비밀번호 찾기·민감작업
|
||||
등 코어가 IDV 를 요구하는 모든 지점에서 이메일 인증 대신 이니시스 팝업이 동작하게
|
||||
합니다.
|
||||
|
||||
결제 PG 플러그인들과 달리 이 플러그인은 실제 개인식별정보(PII — 이름·생년월일·성별·CI/DI
|
||||
등)를 직접 보관합니다. 본인확인 결과를 저장해 두어야 이후 재확인 없이 "이 사용자가
|
||||
본인확인을 완료했는가"를 즉시 판단할 수 있기 때문입니다. `sirsoft-ecommerce`를 비롯한
|
||||
어떤 다른 확장에도 의존하지 않고 코어만으로 동작합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 주요 기능
|
||||
|
||||
<!-- @intent START -->
|
||||
| 영역 | 설명 |
|
||||
|---|---|
|
||||
| 본인확인 | KG이니시스 통합인증(reqSvcCd=03) 팝업 기반 본인확인 |
|
||||
| 성인인증 | 만 19세 이상 여부만 확인하는 별도 purpose |
|
||||
| 중복가입 차단 | DI 또는 CI 기준으로 동일인 재가입 차단 (선택) |
|
||||
| 게스트 처리 | 비로그인 사용자의 본인확인 결과를 임시 보관 후 가입 완료 시 흡수 |
|
||||
| 개인정보 정리 | 사용자 탈퇴/삭제 시 보관 중인 PII 레코드 자동 파기 |
|
||||
| 마이페이지 | 본인확인 완료 상태 카드 노출 |
|
||||
| 관리자 설정 | 테스트/라이브 모드 전환, 중복가입 판정 기준 설정 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 동작 방식
|
||||
|
||||
<!-- @intent START -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[코어가 IDV 요구 · 428] --> B[사용자 클릭 → 팝업 오픈]
|
||||
B --> C[이니시스 인증 폼 제출]
|
||||
C --> D[인증 완료 → 결과 회수]
|
||||
D --> E[SEED 복호화 → PII 확보]
|
||||
E -->|로그인 사용자| F[레코드 즉시 저장]
|
||||
E -->|비로그인 사용자| G[Cache 임시 보관]
|
||||
G --> H[가입 완료 시 레코드로 흡수]
|
||||
```
|
||||
|
||||
팝업은 반드시 사용자 클릭 이벤트 안에서 열립니다 — 비동기 콜백 이후에 열면 브라우저
|
||||
팝업 차단기에 걸립니다. 비로그인 사용자(회원가입 도중)의 본인확인 결과는 가입이 실제로
|
||||
완료되기 전까지 DB 가 아니라 Cache 에만 임시 보관됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 요구 사항
|
||||
|
||||
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| G7 코어 | `>=7.0.8` |
|
||||
| PHP | `^8.2` |
|
||||
<!-- @generated:requirements END -->
|
||||
|
||||
## 설치
|
||||
|
||||
<!-- @generated:install START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
```bash
|
||||
# 번들 설치 (코어에 동봉된 소스에서 설치)
|
||||
php artisan plugin:install sirsoft-verification_kginicis
|
||||
|
||||
# 활성화
|
||||
php artisan plugin:activate sirsoft-verification_kginicis
|
||||
|
||||
# 업데이트 (번들 소스 기준 강제 반영)
|
||||
php artisan plugin:update sirsoft-verification_kginicis --force
|
||||
```
|
||||
|
||||
저장소: https://github.com/gnuboard/g7-plugin-sirsoft-verification_kginicis
|
||||
<!-- @generated:install END -->
|
||||
|
||||
## 관리자 설정
|
||||
|
||||
<!-- @generated:settings-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 의미 | 기본값 |
|
||||
|---|---|---|
|
||||
| `is_test_mode` | 테스트 모드 | `true` |
|
||||
| `test_mid` | 테스트 MID | `INIiasTest` |
|
||||
| `test_api_key` | 테스트 API 키 | `TGdxb2l3enJDWFRTbTgvREU3MGYwUT09` |
|
||||
| `live_mid` | 라이브 MID | - |
|
||||
| `live_api_key` | 라이브 API 키 | - |
|
||||
| `duplicate_field` | 중복 판정 필드 | `di` |
|
||||
| `duplicate_block_enabled` | 중복 가입 차단 | `true` |
|
||||
|
||||
개발자용 상세(타입·검증·저장 위치)는 [설정 스키마](docs/settings.md#설정-스키마) 를 보세요.
|
||||
<!-- @generated:settings-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`live_mid`는 실제 사용 시점(요청 조립 시)에 `SRB` 프리픽스가 자동으로 붙으므로 프리픽스
|
||||
없이 입력해도 됩니다.
|
||||
`live_mid`/`live_api_key`는 `is_test_mode`를 끄는(라이브 모드) 순간부터 필수가 됩니다 —
|
||||
테스트 모드에서는 비워둘 수 있습니다. `duplicate_field`(`di` 또는 `ci`)와
|
||||
`duplicate_block_enabled`는 다른 IDV provider(예: `sirsoft-verification_nhnkcp`)와 별개로
|
||||
이 provider 를 통해 확인한 사용자에게만 적용됩니다. 라이브 API 키는 외부에 노출하지
|
||||
마세요.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 사용 방법
|
||||
|
||||
<!-- @intent START -->
|
||||
**활성화하기**: 플러그인을 활성화하면 자동으로 코어 IDV provider 목록에 등록됩니다.
|
||||
별도 화면 배치 작업 없이 코어가 이미 정의한 IDV 강제 지점(회원가입 등)에서 즉시
|
||||
동작합니다. 여러 IDV provider 를 동시에 설치했다면 코어 본인인증 정책 화면에서 어느
|
||||
provider 를 쓸지 선택합니다.
|
||||
|
||||
**중복가입 차단 켜기**: 동일인이 여러 계정을 만드는 것을 막고 싶다면
|
||||
`duplicate_block_enabled`를 켜고 `duplicate_field`로 DI/CI 중 판정 기준을 고릅니다.
|
||||
|
||||
**개인정보 보관 정책 확인**: 사용자가 탈퇴하거나 관리자가 계정을 삭제하면 보관 중인
|
||||
본인확인 PII 가 자동으로 파기됩니다 — 별도 운영 작업이 필요 없습니다.
|
||||
|
||||
전체 API 목록은 [docs/api/](docs/api/README.md) 를, 발행/구독 훅 목록은
|
||||
[docs/extension-points.md](docs/extension-points.md) 를 참고하세요.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 다른 확장과의 연동
|
||||
|
||||
<!-- @generated:integrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
없음 — 코어만으로 동작합니다.
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
없음.
|
||||
<!-- @generated:integrations END -->
|
||||
|
||||
## 문서
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
<!-- @intent START -->
|
||||
| 증상 | 원인 | 조치 |
|
||||
|---|---|---|
|
||||
| 인증 버튼을 눌러도 팝업이 안 뜸 | 브라우저 팝업 차단기 | `startAuth`가 클릭 이벤트 핸들러 안에서 직접 호출되는지 확인 — 비동기 콜백 뒤로 옮기면 차단됨 |
|
||||
| 설정 저장 시 422 오류 | 라이브 모드인데 `live_mid`/`live_api_key` 미입력 | `is_test_mode`를 켜거나 라이브 자격증명을 입력 |
|
||||
| 이미 가입된 사용자인데 중복 오류 없이 재가입됨 | `duplicate_block_enabled`가 꺼져 있거나 `duplicate_field` 기준이 실제 판정과 다름 | 관리자 설정에서 두 값을 확인 |
|
||||
| 탈퇴한 사용자의 본인확인 정보가 남아있는 것으로 보임 | 정리 리스너 실행 여부를 별도로 확인하지 않음 | `CleanInicisRecordOnUserWithdraw`/`CleanInicisRecordOnUserDelete` 정상 등록 여부를 훅 캐시에서 확인 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 변경 이력
|
||||
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,22 @@
|
||||
# KG이니시스 본인인증 개발자 문서
|
||||
|
||||
> plugins/_bundled/sirsoft-verification_kginicis · 플러그인
|
||||
|
||||
<!-- @generated:stats START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**훅 수**: 3 · **구독 훅 수**: 6 · **라우트 수**: 2 · **모델 수**: 2 · **테이블 수**: 2 · **마이그레이션 수**: 3 · **레이아웃 수**: 1 · **핸들러 수**: 1
|
||||
<!-- @generated:stats END -->
|
||||
|
||||
## 문서 목차
|
||||
|
||||
<!-- @generated:doc-toc START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | 설계 의도·계층 지도·디렉토리 맵 |
|
||||
| [extension-points.md](extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 |
|
||||
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
|
||||
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
|
||||
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
|
||||
| [api/](api/README.md) | API 레퍼런스 |
|
||||
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
|
||||
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
|
||||
<!-- @generated:doc-toc END -->
|
||||
@@ -0,0 +1,68 @@
|
||||
# KG이니시스 본인인증 — 아키텍처
|
||||
|
||||
> 설계 의도와 계층 구조 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설계 의도
|
||||
|
||||
<!-- @intent START -->
|
||||
KG이니시스 본인확인을 코어 IDV(본인인증) 체계에 연결하는 Provider 입니다. 코어
|
||||
`IdentityVerificationInterface`를 구현하는 것이 이 플러그인의 유일한 계약이며, 코어는
|
||||
`core.identity.registered_providers` 필터로 등록된 provider 목록만 알고 어느 PG사의
|
||||
구현인지는 모릅니다.
|
||||
|
||||
결제 PG 플러그인들과 달리 이 플러그인은 실제 PII 를 소유합니다(§data-model.md). 본인확인
|
||||
결과(CI/DI 등)를 재확인 없이 판단하려면 그 결과를 어딘가 보관해야 하고, 그 보관 책임은
|
||||
Provider 자신에게 있습니다 — 코어는 "본인확인 완료 여부"만 알면 되고 원본 PII 를 알 필요가
|
||||
없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 계층 지도
|
||||
|
||||
<!-- @intent START -->
|
||||
```text
|
||||
Controller (Http/Controllers) → FormRequest (Http/Requests)
|
||||
→ InicisIdentityProvider (IdentityVerificationInterface 구현 — verify/challenge 표준 진입점)
|
||||
→ InicisGatewayInterface (외부 통신 + SEED 복호화)
|
||||
→ InicisChallengeMappingRepositoryInterface (mTxId ↔ challenge_id)
|
||||
→ InicisIdentityRecordRepositoryInterface (PII record)
|
||||
→ CacheInterface (비로그인 verify PII 임시 stash)
|
||||
|
||||
Listener (RegisterInicisProviderListener 등)
|
||||
→ 코어 identity/auth/user 훅에 등록 (컴파일 타임 결합 없음)
|
||||
```
|
||||
|
||||
`InicisIdentityProvider`가 4개의 협력자(게이트웨이·Repository 2종·캐시)를 생성자 주입받는
|
||||
것은 그 각각이 서로 다른 관심사(외부 API 통신, DB 영속, 임시 캐시)이기 때문입니다 — 하나로
|
||||
합치면 단위 테스트에서 외부 API 를 모킹할 때 DB/캐시까지 함께 모킹해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 디렉토리
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Http/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Http/Resources/` | API 리소스 | 목록 응답은 화면이 실제로 그리는 것만 싣는다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Repositories/` | 데이터 접근 | 목록 쿼리는 컬럼 프루닝·정렬 화이트리스트 확인 |
|
||||
| `src/Models/` | Eloquent 모델 | 스키마 변경 시 마이그레이션 + 업그레이드 스텝 동반 |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/Enums/` | 상태·타입·분류 | 문자열 리터럴 대신 Enum 을 SSoT 로 둔다 |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `database/migrations/` | 마이그레이션 | 한국어 comment + `down()` 필수, 기설치본은 업그레이드 스텝으로 백필 |
|
||||
| `upgrades/` | 업그레이드 스텝 | DB·설정 구조 변경 시 작성 (모듈/플러그인 전용) |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-verification_kginicis --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-verification_kginicis --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-verification_kginicis --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-verification_kginicis --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-verification_kginicis --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
@@ -0,0 +1,95 @@
|
||||
# KG이니시스 본인인증 — 데이터 모델
|
||||
|
||||
> 모델·소유 테이블·마이그레이션·Enum · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 모델
|
||||
|
||||
<!-- @generated:models START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 모델 | 테이블 | fillable | 관계 | 특성 |
|
||||
|---|---|---|---|---|
|
||||
| `InicisChallengeMapping` | `inicis_challenge_mappings` | 2 | challenge→IdentityVerificationLog | - |
|
||||
| `InicisIdentityRecord` | `inicis_identity_records` | 17 | user→User | - |
|
||||
<!-- @generated:models END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`InicisIdentityRecord`가 PII(이름·생년월일·성별·CI/DI 등)를 직접 보관하는 것이 결제
|
||||
플러그인들과의 근본적 차이입니다(§AGENTS.md "1. 이 확장은 무엇인가") — 이 레코드가 없으면
|
||||
"이 사용자가 본인확인을 완료했는가"를 매번 이니시스에 재조회해야 합니다.
|
||||
`InicisChallengeMapping`은 별도 모델입니다 — 진행 중인 인증 시도(mTxId ↔ challenge_id)와
|
||||
완료된 확인 결과(PII record)는 생명주기가 다르기 때문입니다(전자는 인증 세션 하나,
|
||||
후자는 사용자당 최신 1건).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 소유 테이블
|
||||
|
||||
<!-- @generated:tables START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 테이블 | 모델 |
|
||||
|---|---|
|
||||
| `inicis_challenge_mappings` | `InicisChallengeMapping` |
|
||||
| `inicis_identity_records` | `InicisIdentityRecord` |
|
||||
<!-- @generated:tables END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`inicis_identity_records.user_id`는 `unique` 제약을 갖습니다 — 한 사용자는 본인확인 결과를
|
||||
1건만 보유하며, 재인증 시 기존 레코드를 갱신합니다(새 레코드를 추가하지 않습니다). 이
|
||||
설계 덕분에 "이 사용자가 본인확인을 완료했는가"는 단순 존재 조회 하나로 판정됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 마이그레이션
|
||||
|
||||
<!-- @generated:migrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
마이그레이션 3개.
|
||||
|
||||
| 파일 | 생성 테이블 | 변경 테이블 | down() |
|
||||
|---|---|---|---|
|
||||
| `2026_05_08_000001_create_inicis_identity_records_table.php` | `inicis_identity_records` | `inicis_identity_records` | ✅ |
|
||||
| `2026_05_08_000002_create_inicis_challenge_mappings_table.php` | `inicis_challenge_mappings` | `inicis_challenge_mappings` | ✅ |
|
||||
| `2026_06_22_000001_make_inicis_record_aux_fields_nullable.php` | - | `inicis_identity_records` | ✅ |
|
||||
<!-- @generated:migrations END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
세 번째 마이그레이션이 `name`/`phone`/`birthday` 암호화 컬럼을 nullable 로 완화한 이유는
|
||||
누락 값을 `null`로 저장하기 위함입니다 — `Crypt::encryptString('')`로 "암호화된 빈
|
||||
문자열"을 저장하면 복호화 시 빈 칸이 나오는 오염 레코드가 됩니다. 정상 경로에서는
|
||||
`verify()` 가드(`INCOMPLETE_IDENTITY`)가 이 신원 핵심값의 누락을 이미 차단하므로 항상
|
||||
채워지며, 이 nullable 화는 가드를 우회한 비정상 입력이 암호화된 빈 문자열로 오염
|
||||
저장되는 것을 막는 방어적 통일입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Enum
|
||||
|
||||
<!-- @generated:enums START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| Enum | backing | case 수 | case |
|
||||
|---|---|---|---|
|
||||
| `InicisDuplicateField` | `string` | 2 | `di`, `ci` |
|
||||
<!-- @generated:enums END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`di`(연계정보)와 `ci`(연계정보의 상위 개념 — 사이트 간 동일인 식별용) 중 어느 필드로 중복
|
||||
가입을 판정할지는 운영자가 설정으로 고릅니다(§settings.md `duplicate_field`). Enum 으로
|
||||
닫힌 것은 이 값이 `AssertNoDuplicateInicisIdentity`의 조회 조건과
|
||||
`InicisIdentityLogQueryRepository`의 컬럼 화이트리스트 양쪽에서 타입 안전하게 재사용되기
|
||||
때문입니다 — 문자열 리터럴이었다면 오타가 조용히 "중복 없음"으로 판정될 위험이 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## Repository
|
||||
|
||||
<!-- @generated:repositories START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 클래스 | 종류 | 설명 |
|
||||
|---|---|---|
|
||||
| `InicisChallengeMappingRepository` | 구현 | `inicis_challenge_mappings` 테이블 Repository 구현체. |
|
||||
| `InicisChallengeMappingRepositoryInterface` | 인터페이스 | 이니시스 mTxId ↔ challenge_id 매핑 Repository 인터페이스. |
|
||||
| `InicisIdentityLogQueryRepository` | 구현 | InicisIdentityLogQueryRepositoryInterface 구현체. |
|
||||
| `InicisIdentityLogQueryRepositoryInterface` | 인터페이스 | 본 plugin 의 동일인 검증 listener 전용 IdentityVerificationLog 조회 Repository. |
|
||||
| `InicisIdentityRecordRepository` | 구현 | `inicis_identity_records` 테이블 Repository 구현체. |
|
||||
| `InicisIdentityRecordRepositoryInterface` | 인터페이스 | KG이니시스 본인확인 PII 레코드 Repository 인터페이스. |
|
||||
<!-- @generated:repositories END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
3쌍(인터페이스+구현체)으로 나뉜 것은 각자 다른 데이터를 다루기 때문입니다 —
|
||||
`InicisChallengeMappingRepository`(진행 중인 인증 세션), `InicisIdentityRecordRepository`
|
||||
(완료된 PII), `InicisIdentityLogQueryRepository`(동일인 검증 전용 `IdentityVerificationLog`
|
||||
조회, 코어 로그 테이블을 이 플러그인 관점으로 좁혀 읽는 어댑터). 결제 플러그인들이
|
||||
Repository 를 하나도 두지 않는 것과 대조적으로, 이 플러그인은 실제 PII 를 소유하므로
|
||||
Repository 인터페이스 주입 원칙(§CLAUDE.md "Service-Repository 패턴")이 그대로 적용됩니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,130 @@
|
||||
# KG이니시스 본인인증 — 확장점
|
||||
|
||||
> 발행/구독 훅·미들웨어·채널·스케줄 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 발행 훅
|
||||
|
||||
<!-- @generated:hooks-published START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
발행 훅 3종 / 호출 지점 3곳. 이 중 3종은 `getHooks()` 선언에 없어 소스에서 자동 감지한 것입니다 — 선언에 추가하면 유형과 설명이 함께 실립니다.
|
||||
|
||||
| 훅 이름 | 유형 | 설명 | 발행 위치 |
|
||||
|---|---|---|---|
|
||||
| `core.auth.after_register` | action | — | `src/Listeners/CompleteInicisRecordAfterRegister.php:64` |
|
||||
| `core.user.after_withdraw` | action | — | `src/Listeners/CleanInicisRecordOnUserWithdraw.php:18` |
|
||||
| `core.user.before_delete` | action | — | `src/Listeners/CleanInicisRecordOnUserDelete.php:23` |
|
||||
<!-- @generated:hooks-published END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
이 3종은 이 플러그인이 실제로 발행하는 훅이 아닙니다 — 세 리스너 파일 모두 코어가 그
|
||||
훅을 어떻게 호출하는지 보여주는 **docblock 예시**(`HookManager::doAction('core.user.before_delete', $user);`
|
||||
형태의 주석)를 갖고 있는데, 소스 자동 감지가 그 주석 텍스트를 실제 발행 호출로 오인해
|
||||
잡아낸 결과입니다. 실제 발행 주체는 코어이며, 이 플러그인은 §구독 훅에서 같은 이름으로
|
||||
**구독**만 합니다. 이 확장의 리스너에 이런 docblock 예시를 새로 추가할 때는 이 표에
|
||||
가짜 항목이 늘어난다는 점을 감안합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 구독 훅
|
||||
|
||||
<!-- @generated:hooks-subscribed START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 훅 이름 | 유형 | 리스너 | 메서드 | 우선순위 |
|
||||
|---|---|---|---|---|
|
||||
| `core.auth.after_register` | action (미선언) | `CompleteInicisRecordAfterRegister` | `handle` | 50 |
|
||||
| `core.auth.before_register` | action (미선언) | `AssertNoDuplicateInicisIdentity` | `handle` | 20 |
|
||||
| `core.identity.registered_providers` | filter | `RegisterInicisProviderListener` | `register` | 20 |
|
||||
| `core.plugin_settings.update_validation_rules` | filter | `ValidateInicisSettingsListener` | `addLiveModeRules` | 10 |
|
||||
| `core.user.after_withdraw` | action (미선언) | `CleanInicisRecordOnUserWithdraw` | `handle` | 50 |
|
||||
| `core.user.before_delete` | action (미선언) | `CleanInicisRecordOnUserDelete` | `handle` | 50 |
|
||||
<!-- @generated:hooks-subscribed END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`AssertNoDuplicateInicisIdentity`가 `core.auth.before_register`(가입 **전**)을 구독하는
|
||||
것은 중복 가입을 막으려면 가입 트랜잭션이 커밋되기 전에 차단해야 하기 때문입니다 —
|
||||
`after_register`에서 잡으면 이미 중복 계정이 생성된 뒤라 롤백이 더 복잡해집니다.
|
||||
`core.user.before_delete`는 `sync: true`로 동기 실행됩니다 — 이 시점 정리가 실패하면
|
||||
FK 제약(1451)으로 사용자 삭제 자체가 실패해야 하므로, 비동기 큐로 미뤄지면 삭제가 먼저
|
||||
끝나버릴 수 있습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 훅 리스너
|
||||
|
||||
<!-- @generated:listeners START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 리스너 | 구독 훅 | 등록 방식 | HookListenerInterface | 파일 |
|
||||
|---|---|---|---|---|
|
||||
| `AssertNoDuplicateInicisIdentity` | 1개 | 명시 등록 | ✅ | `src/Listeners/AssertNoDuplicateInicisIdentity.php` |
|
||||
| `CleanInicisRecordOnUserDelete` | 1개 | 명시 등록 | ✅ | `src/Listeners/CleanInicisRecordOnUserDelete.php` |
|
||||
| `CleanInicisRecordOnUserWithdraw` | 1개 | 명시 등록 | ✅ | `src/Listeners/CleanInicisRecordOnUserWithdraw.php` |
|
||||
| `CompleteInicisRecordAfterRegister` | 1개 | 명시 등록 | ✅ | `src/Listeners/CompleteInicisRecordAfterRegister.php` |
|
||||
| `RegisterInicisProviderListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/RegisterInicisProviderListener.php` |
|
||||
| `ValidateInicisSettingsListener` | 1개 | 명시 등록 | ✅ | `src/Listeners/ValidateInicisSettingsListener.php` |
|
||||
<!-- @generated:listeners END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
6개 리스너 중 3개(`CompleteInicisRecordAfterRegister`, `CleanInicisRecordOnUserWithdraw`,
|
||||
`CleanInicisRecordOnUserDelete`)는 사용자 생명주기 각 단계(가입 완료·탈퇴·삭제)에 맞춰
|
||||
PII 레코드를 흡수하거나 정리하는 대칭 구조입니다 — 하나를 고칠 때 나머지 둘도 같은 PII
|
||||
필드를 다루고 있는지 확인해야 합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 레이아웃 확장
|
||||
|
||||
<!-- @generated:layout-extensions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 대상 | 설명 |
|
||||
|---|---|
|
||||
| `resources/extensions/identity_provider_inicis.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
| `resources/extensions/mypage_identity_card.json` | 다른 확장/템플릿 레이아웃에 주입되는 조각 |
|
||||
<!-- @generated:layout-extensions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`identity_provider_inicis.json`은 코어 IDV 팝업(§CLAUDE.md "본인인증(IDV) 공통 UI 가이드")이
|
||||
provider 별로 다른 안내 문구·로고를 보여줘야 할 때 이 플러그인이 자기 몫을 주입하는
|
||||
조각입니다. `mypage_identity_card.json`은 마이페이지에 "본인확인 완료" 상태 카드를
|
||||
보여주는 조각으로, `sirsoft-verification_nhnkcp`도 동일한 명명 규칙의 자기 조각을 갖습니다
|
||||
— 여러 IDV provider 가 동시에 설치돼도 각자 자기 카드만 주입하므로 충돌하지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 미들웨어
|
||||
|
||||
<!-- @generated:middleware START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 미들웨어가 없습니다._
|
||||
<!-- @generated:middleware END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 플러그인들과 달리 이 플러그인은 PG 서버가 직접 호출하는 웹훅/통보 엔드포인트가
|
||||
없습니다 — 본인확인 결과는 팝업 콜백(사용자 브라우저 경유)으로만 도달하므로 IP
|
||||
화이트리스트 같은 서버간 통신 검증이 필요 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 브로드캐스트 채널
|
||||
|
||||
<!-- @generated:channels START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 브로드캐스트 채널이 없습니다._
|
||||
<!-- @generated:channels END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
인증 결과는 팝업의 postMessage 또는 팝업 종료 감지로 프론트가 직접 회수합니다(§AGENTS.md
|
||||
"핵심 흐름") — 서버가 다른 클라이언트에 실시간으로 알려야 할 상태 변화가 없어 브로드캐스트
|
||||
채널이 필요 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 스케줄
|
||||
|
||||
<!-- @generated:schedules START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 스케줄이 없습니다._
|
||||
<!-- @generated:schedules END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
mTxId ↔ challenge_id 매핑(`inicis_challenge_mappings`)이나 pending PII 캐시는 만료된
|
||||
항목을 별도 배치로 청소하지 않습니다 — 캐시는 TTL 로 자연 소멸하고, 매핑 테이블의 정리는
|
||||
사용자 삭제/탈퇴 시점 리스너가 담당합니다(§훅 리스너).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 알림 정의
|
||||
|
||||
<!-- @generated:notifications START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 알림 정의가 없습니다._
|
||||
<!-- @generated:notifications END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
본인확인 성공/실패는 사용자가 팝업 화면에서 즉시 확인하는 동기적 상호작용이라, 별도
|
||||
알림(이메일/SMS 등)을 발송할 지점이 없습니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,81 @@
|
||||
# KG이니시스 본인인증 — 프론트엔드
|
||||
|
||||
> 레이아웃·액션 핸들러·전역 진입점·에셋 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 레이아웃
|
||||
|
||||
<!-- @generated:layouts START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
레이아웃 1개 (루트: `resources/layouts`).
|
||||
|
||||
| 그룹 | 개수 |
|
||||
|---|---|
|
||||
| `admin` | 1개 |
|
||||
|
||||
| 레이아웃 | 그룹 | 종류 | extends |
|
||||
|---|---|---|---|
|
||||
| `plugin_settings` | `admin` | 화면 | `_admin_base` |
|
||||
<!-- @generated:layouts END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 PG 플러그인들과 마찬가지로 이 플러그인이 소유한 화면 레이아웃은 관리자 설정 화면
|
||||
하나뿐입니다 — 회원가입·마이페이지의 본인확인 UI는 이 플러그인 소유가 아니라
|
||||
§레이아웃 확장(다른 확장/템플릿 레이아웃에 주입되는 조각) 및 코어 IDV 공통 팝업 UI로
|
||||
존재합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 액션 핸들러
|
||||
|
||||
<!-- @generated:handlers START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
핸들러 1개 (정의: `resources/js/index.ts`).
|
||||
|
||||
| 핸들러 | 레이아웃에서 부르는 이름 |
|
||||
|---|---|
|
||||
| `startAuth` | `sirsoft-verification_kginicis.startAuth` |
|
||||
<!-- @generated:handlers END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`startAuth`는 반드시 사용자 클릭 이벤트 핸들러 안에서(동기적으로) 호출돼야 합니다 —
|
||||
`window.open`으로 빈 팝업을 먼저 연 뒤 그 팝업에 이니시스 인증 폼을 제출하는 방식인데,
|
||||
`window.open`이 사용자 제스처 컨텍스트 밖(예: API 응답을 기다린 뒤의 비동기 콜백)에서
|
||||
호출되면 Chrome 등 브라우저의 팝업 차단기에 걸립니다. 과거 `setLauncher`로 코어 IDV
|
||||
런처를 덮어써 챌린지 발급 이후 시점에 팝업을 여는 방식을 썼다가 이 문제로 폐기됐습니다
|
||||
(소스 주석 "Phase E′-revert").
|
||||
<!-- @intent END -->
|
||||
|
||||
## 전역 진입점
|
||||
|
||||
<!-- @generated:frontend-entry START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 엔트리 파일 | `resources/js/index.ts` |
|
||||
| 전역 객체 | `window.__SirsoftVerificationKginicis` |
|
||||
| 재등록 진입점 | `initPlugin()` |
|
||||
|
||||
로케일 전환 시 코어가 이 진입점을 호출해 핸들러를 다시 등록합니다. 진입점은 핸들러 재등록만 수행하고 1회성 부팅 작업을 포함하지 않습니다.
|
||||
<!-- @generated:frontend-entry END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`window.__SirsoftVerificationKginicis`로 노출되는 이유는 코어가 로케일 전환 시 이 이름으로
|
||||
재등록 진입점을 찾기 때문입니다(§CLAUDE.md "재등록 진입점"). 이 플러그인은 결제 PG
|
||||
플러그인들과 달리 결제창 SDK 를 동적 로드하지 않습니다 — 이니시스 인증 폼은 팝업
|
||||
안에서 서버가 렌더링한 페이지로 제출되므로 프론트가 별도 스크립트를 불러올 필요가
|
||||
없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 에셋
|
||||
|
||||
<!-- @generated:assets START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 구분 |
|
||||
|---|---|
|
||||
| `dist/js/plugin.iife.js` | 빌드 산출물 (커밋 대상) |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 (manifest) |
|
||||
|
||||
로딩 설정: `{"strategy":"global","priority":100,"dependencies":[]}`
|
||||
<!-- @generated:assets END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
제3자 SDK 스크립트가 목록에 없는 것은 §전역 진입점에서 설명한 대로 이 플러그인이 그런
|
||||
자산을 동적 로드하지 않기 때문입니다 — 인증 폼 자체가 서버 렌더링 페이지라 프론트는
|
||||
팝업을 열고 결과를 회수하는 역할만 합니다. CSS 산출물이 없는 것은 이 플러그인의 UI가
|
||||
버튼·상태 카드 같은 최소한의 코어 컴포넌트로만 구성되기 때문입니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,93 @@
|
||||
# KG이니시스 본인인증 — 설정·권한·라우트
|
||||
|
||||
> 설정 스키마·권한·메뉴·라우트·의존 관계 · 진입점: [AGENTS.md](../AGENTS.md)
|
||||
|
||||
## 설정 스키마
|
||||
|
||||
<!-- @generated:settings-schema START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 타입 | 기본값 | 설명 |
|
||||
|---|---|---|---|
|
||||
| `is_test_mode` | `boolean` | `true` | 테스트 모드 |
|
||||
| `test_mid` | `string` | `INIiasTest` | 테스트 MID |
|
||||
| `test_api_key` | `string` | `TGdxb2l3enJDWFRTbTgvREU3MGYwUT09` | 테스트 API 키 |
|
||||
| `live_mid` | `string` | - | 라이브 MID |
|
||||
| `live_api_key` | `string` | - | 라이브 API 키 |
|
||||
| `duplicate_field` | `enum` | `di` | 중복 판정 필드 |
|
||||
| `duplicate_block_enabled` | `boolean` | `true` | 중복 가입 차단 |
|
||||
|
||||
기본값 파일: `config/settings/defaults.json` · 설정 화면 레이아웃: `resources/layouts/admin/plugin_settings.json`
|
||||
<!-- @generated:settings-schema END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 PG 플러그인들의 `test_*`/`live_*` 쌍과 같은 구조이지만 필드는 단 2개(`test_mid`/
|
||||
`test_api_key`, `live_mid`/`live_api_key`)뿐입니다 — 본인확인 API 는 결제와 달리 사인키·
|
||||
INIAPI 키·모바일 해시키처럼 기능별로 분리된 자격증명이 필요 없습니다. `live_mid`는
|
||||
`InicisIdentityProvider::buildLiveMid()`가 그 값을 요청 조립 시점에 쓸 때 `SRB` 프리픽스를
|
||||
동적으로 부착하므로(DB 저장값은 원본 그대로) 운영자가 프리픽스를 직접 입력할 필요가
|
||||
없습니다. `live_mid`/`live_api_key`는 `is_test_mode=false`일 때만
|
||||
`ValidateInicisSettingsListener`가 `required`로 강제합니다(§AGENTS.md "핵심 흐름").
|
||||
`duplicate_field`/`duplicate_block_enabled`는 `sirsoft-ecommerce`와 무관한 코어 레벨
|
||||
회원가입 규칙입니다 — 이 플러그인은 이커머스에 의존하지 않습니다(§의존 관계).
|
||||
<!-- @intent END -->
|
||||
|
||||
## 권한
|
||||
|
||||
<!-- @generated:permissions START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_선언된 권한이 없습니다._
|
||||
<!-- @generated:permissions END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
본인인증 설정 접근 권한은 코어의 관리자 권한 체계 안에서 다뤄집니다 — IDV provider 마다
|
||||
별도 권한을 선언하면 provider 를 여러 개 설치했을 때 "본인인증 설정을 볼 수 있는 사람"이
|
||||
provider 수만큼 중복 정의됩니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 메뉴
|
||||
|
||||
<!-- @generated:menus START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
_등록하는 메뉴가 없습니다._
|
||||
<!-- @generated:menus END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
설정 화면(`plugin_settings.json`)은 코어의 "플러그인 관리 > 설정" 공통 진입점을 통해
|
||||
접근합니다 — IDV provider 마다 전용 사이드바 메뉴를 만들면 provider 를 여러 개 설치했을
|
||||
때 메뉴가 난립합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 라우트
|
||||
|
||||
<!-- @generated:routes START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 파일 | URL prefix |
|
||||
|---|---|---|
|
||||
| `api` | `src/routes/api.php` | `/api/plugins/sirsoft-verification_kginicis/...` |
|
||||
| `web` | `src/routes/web.php` | `/plugins/sirsoft-verification_kginicis/...` |
|
||||
|
||||
확장 라우트는 **활성 상태인 확장의 것만** 등록됩니다. 라우트 정의를 바꾸면 라우트 캐시 재생성이 필요합니다.
|
||||
<!-- @generated:routes END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`web` 라우트는 CSRF 검증을 명시적으로 제외합니다(`ValidateCsrfToken` 제외 그룹) — 이니시스
|
||||
결제창이 팝업 안에서 우리 서버로 폼을 직접 POST 하는데, 그 요청은 우리 CSRF 토큰을 모르기
|
||||
때문입니다. `popup-bridge`는 팝업과 opener 창 사이의 postMessage 중계용 정적 페이지라
|
||||
별도 인증이 필요 없습니다. `api`는 로그인 사용자가 Bearer 토큰으로 자기 본인확인 상태를
|
||||
조회하는 마이페이지 엔드포인트(`GET /me/identity/inicis`)입니다 — 챌린지 시작 자체는
|
||||
코어 `/api/identity/challenges`가 담당하므로 이 플러그인의 `api` 라우트에는 없습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 의존 관계
|
||||
|
||||
<!-- @generated:dependencies START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
없음 — 코어만으로 동작합니다.
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
없음.
|
||||
<!-- @generated:dependencies END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
결제 PG 플러그인들과 달리 이 플러그인은 `sirsoft-ecommerce`에 의존하지 않습니다 — 본인확인은
|
||||
결제와 무관하게 회원가입·비밀번호 찾기 등 코어 인증 흐름 전반에 쓰이는 기능이라, 이커머스가
|
||||
설치되지 않은 사이트(게시판만 운영하는 등)에서도 단독으로 동작해야 합니다.
|
||||
<!-- @intent END -->
|
||||
@@ -0,0 +1,189 @@
|
||||
# NHN KCP 휴대폰 본인확인 — 에이전트 가이드
|
||||
|
||||
> 이 문서는 이 플러그인을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 [README.md](README.md) 를 보세요.
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 유형: 플러그인 (sirsoft-verification_nhnkcp) — NHN KCP 휴대폰 본인확인 IDV Provider. 코어 `IdentityVerificationInterface` 구현, PII 레코드 소유 (`sirsoft-verification_kginicis`와 같은 부류·다른 벤더)
|
||||
2. 확장 방식: `RegisterKcpProviderListener` 로 코어 `core.identity.registered_providers` 필터에 등록 — 코어는 이 플러그인의 존재를 모른다
|
||||
3. 건드리면 안 되는 것: 데스크톱 팝업/모바일 리다이렉트 분기(`isMobileEnvironment()`) 우회, 라이브 사이트코드 `SM` 프리픽스 정책값 상수(`LIVE_SITE_CD_PREFIX`) 미참조, 중복가입 차단(`AssertNoDuplicateKcpIdentity`) 우회
|
||||
4. 작업 위치: `plugins/_bundled/sirsoft-verification_nhnkcp` — 활성 디렉토리 직접 수정 금지
|
||||
5. 반영: `php artisan plugin:update sirsoft-verification_nhnkcp --force`
|
||||
```
|
||||
|
||||
## 1. 이 확장은 무엇인가
|
||||
|
||||
<!-- @intent START -->
|
||||
NHN KCP 휴대폰 본인확인을 코어 본인인증(IDV) 체계에 연결하는 Provider 입니다.
|
||||
`sirsoft-verification_kginicis`와 같은 부류(코어 `IdentityVerificationInterface` 12메서드
|
||||
구현)지만 다른 벤더이며, 운영자는 둘 중 하나 또는 둘 다 설치해 사용할 수 있습니다 —
|
||||
코어는 등록된 provider ID로만 구분합니다.
|
||||
|
||||
**이 플러그인만의 차이**: 인증 화면 진입 방식이 기기별로 갈립니다 — 데스크톱은
|
||||
`sirsoft-verification_kginicis`와 동일하게 사용자 클릭 컨텍스트 안에서 빈 팝업을 열고
|
||||
그 안에서 인증을 진행하지만, 모바일은 팝업 대신 **전체 페이지 리다이렉트**로 KCP 인증
|
||||
화면으로 이동한 뒤 콜백으로 복귀합니다(`isMobileEnvironment()` 분기) — 모바일 브라우저는
|
||||
팝업 UX 가 나쁘고 앱 전환(문자 인증 등)이 얽히면 팝업 컨텍스트 자체가 끊기기 쉽기
|
||||
때문입니다. 리다이렉트 복귀 시 원래 상태를 되살리기 위해 `sessionStorage`에
|
||||
`g7.identity.redirectStash` 키로 복귀 정보를 임시 저장합니다.
|
||||
|
||||
**설계 원칙**: 이 플러그인도 실제 PII 를 소유합니다(§data-model.md — `kginicis`와 같은
|
||||
구조: 완료된 확인 결과 레코드 + 진행 중인 인증 거래 매핑을 별도 테이블로 분리).
|
||||
|
||||
**의도적으로 하지 않는 것**: `kginicis`와 동일하게, 비로그인 사용자의 PII 는 확인 즉시
|
||||
DB 에 쓰지 않고 임시 보관했다가 가입 완료 시에만 레코드로 흡수합니다. 사용자 탈퇴/삭제
|
||||
시 관련 PII 를 자동 정리합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 2. 디렉토리 지도
|
||||
|
||||
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 경로 | 역할 | 수정 시 필요한 절차 |
|
||||
|---|---|---|
|
||||
| `plugin.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json·composer.json 동기화 |
|
||||
| `plugin.php` | 진입 클래스 (선언형 표면 SSoT) | 표면 변경 시 `ext:docgen` 재실행 + 코어 최소 버전 검토 |
|
||||
| `src/Http/Controllers/` | 컨트롤러 | API 표면 변경 시 `api:docgen` 재실행 |
|
||||
| `src/Http/Requests/` | FormRequest (검증 SSoT) | 검증 규칙은 Service 가 아니라 여기에 둔다 |
|
||||
| `src/Http/Resources/` | API 리소스 | 목록 응답은 화면이 실제로 그리는 것만 싣는다 |
|
||||
| `src/Services/` | 비즈니스 로직 | Repository 인터페이스 주입 (구체 클래스 금지) |
|
||||
| `src/Repositories/` | 데이터 접근 | 목록 쿼리는 컬럼 프루닝·정렬 화이트리스트 확인 |
|
||||
| `src/Models/` | Eloquent 모델 | 스키마 변경 시 마이그레이션 + 업그레이드 스텝 동반 |
|
||||
| `src/Listeners/` | 훅 리스너 | Repository 경유 (Model·DB 파사드 직접 접근 금지) |
|
||||
| `src/Enums/` | 상태·타입·분류 | 문자열 리터럴 대신 Enum 을 SSoT 로 둔다 |
|
||||
| `src/routes/` | 라우트 | 모든 라우트에 `name()` 필수 |
|
||||
| `database/migrations/` | 마이그레이션 | 한국어 comment + `down()` 필수, 기설치본은 업그레이드 스텝으로 백필 |
|
||||
| `resources/layouts/` | 레이아웃 JSON | `php artisan plugin:update sirsoft-verification_nhnkcp --force` (빌드 불필요) |
|
||||
| `resources/js/` | 프론트 엔트리·핸들러 | `php artisan plugin:build` → `php artisan plugin:update sirsoft-verification_nhnkcp --force` |
|
||||
| `resources/extensions/` | 다른 확장 레이아웃에 주입하는 조각 | `php artisan plugin:update sirsoft-verification_nhnkcp --force` |
|
||||
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan plugin:update sirsoft-verification_nhnkcp --force` |
|
||||
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
|
||||
| `config/` | 확장 config | 설정 기본값은 settings 스키마와 어긋나지 않게 |
|
||||
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
|
||||
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
|
||||
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan plugin:update sirsoft-verification_nhnkcp --force` |
|
||||
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
|
||||
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
|
||||
<!-- @generated:directory-map END -->
|
||||
|
||||
## 3. 핵심 흐름
|
||||
|
||||
<!-- @intent START -->
|
||||
**본인확인 시작~완료(데스크톱)**: 코어 428 응답 → `startAuth`가 사용자 클릭 컨텍스트
|
||||
안에서 `window.open`으로 빈 팝업 생성 → KCP 인증 폼 제출 → 인증 완료 후
|
||||
`KcpCertTransactionRepository`가 거래 매핑을 저장 → 팝업 종료 감지로 결과 회수 →
|
||||
`KcpIdentityProvider::verify()`가 결과를 반환. 성인인증(`nhnkcp.adult_verification`
|
||||
purpose)으로 발행된 challenge 는 만 19세 이상만 통과시킵니다.
|
||||
|
||||
**본인확인 시작~완료(모바일)**: `isMobileEnvironment()`가 참이면 팝업 대신 전체 페이지
|
||||
리다이렉트로 KCP 인증 화면으로 이동 → 복귀 정보를 `sessionStorage`(`g7.identity.redirectStash`)에
|
||||
저장 → 인증 완료 후 콜백 URL로 복귀 → 저장된 stash 로 원래 challenge 컨텍스트를 복원.
|
||||
|
||||
**비로그인 사용자 처리 / 중복가입 차단 / 사용자 삭제·탈퇴 시 정리**:
|
||||
`sirsoft-verification_kginicis`와 동일한 3단계 패턴입니다 — `verify()` 시 Cache
|
||||
stash(`nhnkcp:pending_record:`) → `core.auth.after_register`에서 흡수,
|
||||
`core.auth.before_register`에서 `AssertNoDuplicateKcpIdentity`가 중복 차단,
|
||||
`core.user.before_delete`/`after_withdraw`에서 PII 정리.
|
||||
|
||||
**설정 저장 시 검증 + 라이브 사이트코드 정규화**: 관리자가 설정 저장 요청 → FormRequest
|
||||
검증 단계에서 `core.plugin_settings.update_validation_rules` 훅 →
|
||||
`ValidateKcpSettingsListener::addLiveModeRules()`가 라이브 모드일 때 `live_site_cd`/
|
||||
`live_enc_key`를 필수로 강제(값이 비어있지 않은지만 확인 — 아직 프리픽스는 보지 않음) →
|
||||
검증 통과 후 `PluginSettingsService`가 실제 저장하는 단계에서
|
||||
`core.plugin_settings.filter_save_data` 훅 → 같은 리스너의 `normalizeLiveSiteCd()`가
|
||||
`live_site_cd`에 `SM` 프리픽스가 없으면 자동으로 붙여 저장. 두 훅이 분리된 이유는 코어의
|
||||
FormRequest 검증과 Service 저장이 서로 다른 파이프라인 단계이기 때문입니다 — "필수값이
|
||||
비어있지 않은가"는 검증 단계에서, "저장될 값의 형식을 교정"은 저장 단계에서 처리합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 4. 확장점
|
||||
|
||||
<!-- @generated:extension-points-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 확장점 | 수 | 상세 |
|
||||
|---|---|---|
|
||||
| 발행 훅 | 0개 | [발행 훅](docs/extension-points.md#발행-훅) |
|
||||
| 구독 훅 | 7개 | [구독 훅](docs/extension-points.md#구독-훅) |
|
||||
| 훅 리스너 | 6개 | [훅 리스너](docs/extension-points.md#훅-리스너) |
|
||||
| 레이아웃 확장 | 2개 | [레이아웃 확장](docs/extension-points.md#레이아웃-확장) |
|
||||
| 미들웨어 | 0개 | [미들웨어](docs/extension-points.md#미들웨어) |
|
||||
| 브로드캐스트 채널 | 0개 | [브로드캐스트 채널](docs/extension-points.md#브로드캐스트-채널) |
|
||||
| 스케줄 | 0개 | [스케줄](docs/extension-points.md#스케줄) |
|
||||
| 알림 정의 | 0개 | [알림 정의](docs/extension-points.md#알림-정의) |
|
||||
<!-- @generated:extension-points-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
발행 훅이 0개인 것은 `sirsoft-verification_kginicis`와의 실제 차이입니다 — kginicis 쪽
|
||||
3건은 리스너 docblock 의 코어 호출 예시 주석을 소스 자동 감지가 오인한 결과였는데(§data-model.md
|
||||
계열의 동일 패턴), 이 플러그인의 리스너 docblock 은 그런 예시 서술 방식을 쓰지 않아
|
||||
오탐이 없습니다. `core.identity.registered_providers`는 코어가 등록된 IDV provider 목록을
|
||||
모으는 필터 훅입니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 5. 수정 시 동반 의무
|
||||
|
||||
- [ ] `_bundled` 에서만 수정하고 `php artisan plugin:update sirsoft-verification_nhnkcp --force` 로 반영
|
||||
- [ ] manifest version 상향 시 `package.json` · `package-lock.json` · `composer.json` 동기화 + CHANGELOG 기재
|
||||
- [ ] 스키마 변경 시 마이그레이션(한국어 comment + `down()`) + 기설치본 백필용 업그레이드 스텝
|
||||
- [ ] API 표면 변경 시 `php artisan api:docgen --scope=plugin:sirsoft-verification_nhnkcp` 재실행 + `docs/api/**` 갱신
|
||||
- [ ] 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
|
||||
- [ ] TSX/TS 변경 시 `--production` 재빌드 후 `dist/` 커밋 (sourceMappingURL 잔존 금지)
|
||||
- [ ] 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
|
||||
- [ ] PII 컬럼을 다루는 코드 변경 시 GDPR 삭제/탈퇴 정리 리스너(`CleanKcpRecordOnUserDelete`/`CleanKcpRecordOnUserWithdraw`)가 여전히 그 컬럼을 정리하는지 확인
|
||||
- [ ] 데스크톱/모바일 분기(`isMobileEnvironment()`)를 고칠 때 양쪽 복귀 경로(팝업 종료 감지 / `redirectStash` 복원)를 함께 테스트
|
||||
- [ ] `duplicate_field`/`duplicate_block_enabled` 로직을 고치면 `KcpDuplicateField` Enum 과 `AssertNoDuplicateKcpIdentity`를 함께 갱신
|
||||
- [ ] `normalizeLiveSiteCd()`/`addLiveModeRules()`를 고칠 때 두 훅(`filter_save_data`/`update_validation_rules`)의 실행 순서(검증 먼저, 정규화는 저장 시점)를 유지
|
||||
|
||||
## 6. 금지 패턴
|
||||
|
||||
<!-- @intent START -->
|
||||
| 금지 | 올바른 사용 | 이유 |
|
||||
|---|---|---|
|
||||
| 비로그인 사용자의 PII 를 verify 즉시 DB 에 저장 | Cache 에 stash(`nhnkcp:pending_record:` 접두) 후 가입 완료 시 흡수 | 가입을 완료하지 않은 방문자의 PII 를 DB 에 영구 저장하면 불필요한 개인정보 보유가 된다 |
|
||||
| 사용자 삭제/탈퇴 리스너 없이 PII 컬럼 추가 | `CleanKcpRecordOnUserDelete`/`CleanKcpRecordOnUserWithdraw`에 정리 로직 동반 | 정리 누락 시 탈퇴한 사용자의 PII 가 무기한 남는다 |
|
||||
| `LIVE_SITE_CD_PREFIX` 상수를 참조하지 않고 `'SM'`을 문자열로 재작성 | `KcpIdentityProvider::LIVE_SITE_CD_PREFIX` 참조 | KCP 사이트코드 정책이 바뀌면 상수 1곳만 갱신해야 런타임 로직 전체에 반영된다 |
|
||||
| 모바일 리다이렉트 복귀 시 `redirectStash`를 검증 없이 신뢰 | 복귀 정보의 challenge 컨텍스트를 서버측 상태와 대조 후 사용 | `sessionStorage`는 클라이언트가 임의로 조작할 수 있는 저장소다 |
|
||||
| 팝업을 사용자 클릭 이벤트 핸들러 밖에서 `window.open` | 사용자 제스처 컨텍스트 안에서 직접 호출 (데스크톱 경로) | Chrome 등 브라우저는 사용자 제스처 없이 열리는 팝업을 자동 차단한다 |
|
||||
| 라이브 암호화 키를 로그·에러 메시지에 노출 | 운영 키는 항상 마스킹하거나 로그 대상에서 제외 | 노출되면 제3자가 본인확인 API 를 위조 호출할 수 있다 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 7. 테스트 실행
|
||||
|
||||
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 종류 | 개수 | 위치 |
|
||||
|---|---|---|
|
||||
| PHPUnit | 20개 | `plugins/_bundled/sirsoft-verification_nhnkcp/tests` |
|
||||
| Vitest | 7개 | `vitest.config.ts` |
|
||||
| Playwright | 2개 | `tests/Playwright` |
|
||||
| 시나리오 매니페스트 | 14개 | `tests/scenarios` |
|
||||
|
||||
기저 TestCase: `tests/PluginTestCase.php` — 확장 테스트는 이 클래스를 상속합니다 (`Tests\TestCase` 직접 상속 금지).
|
||||
|
||||
```bash
|
||||
# PHPUnit (변경 범위만) (Bash)
|
||||
php vendor/bin/phpunit plugins/_bundled/sirsoft-verification_nhnkcp/tests --filter='<대상클래스>'
|
||||
|
||||
# Vitest (확장 디렉토리에서) (PowerShell)
|
||||
cd plugins/_bundled/sirsoft-verification_nhnkcp && powershell -Command "npm run test:run -- <대상>"
|
||||
|
||||
# Playwright E2E (Bash)
|
||||
npx playwright test plugins/_bundled/sirsoft-verification_nhnkcp/tests/Playwright/specs/<대상>.spec.ts
|
||||
|
||||
```
|
||||
|
||||
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
|
||||
<!-- @generated:test-commands END -->
|
||||
|
||||
## 8. 문서 목차
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
@@ -0,0 +1,186 @@
|
||||
# NHN KCP 휴대폰 본인확인
|
||||
|
||||
**G7 플러그인 · sirsoft-verification_nhnkcp**
|
||||
NHN KCP 휴대폰 본인확인(V2 REST)을 G7 코어 IDV 인프라에 Provider 로 등록하는 플러그인
|
||||
|
||||
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-1.0.1-0066FF?style=flat-square" alt="version 1.0.1">
|
||||
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
|
||||
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.6-1F883D?style=flat-square" alt="G7 >=7.0.6">
|
||||
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
|
||||
</p>
|
||||
<!-- @generated:badges END -->
|
||||
|
||||
---
|
||||
|
||||
[소개](#소개) · [주요 기능](#주요-기능) · [동작 방식](#동작-방식) · [요구 사항](#요구-사항) · [설치](#설치) · [관리자 설정](#관리자-설정) · [사용 방법](#사용-방법) · [다른 확장과의 연동](#다른-확장과의-연동) · [문서](#문서) · [트러블슈팅](#트러블슈팅) · [변경 이력](#변경-이력) · [라이선스](#라이선스)
|
||||
|
||||
---
|
||||
|
||||
## 소개
|
||||
|
||||
<!-- @intent START -->
|
||||
NHN KCP 휴대폰 본인확인을 G7 코어의 본인인증(IDV) 체계에 연결하는 플러그인입니다.
|
||||
`sirsoft-verification_kginicis`와 같은 부류(코어 표준 인터페이스 구현)지만 다른 벤더이며,
|
||||
운영자는 둘 중 하나 또는 둘 다 설치해 사용할 수 있습니다.
|
||||
|
||||
이 플러그인만의 특징은 인증 화면 진입 방식이 기기별로 갈린다는 점입니다 — 데스크톱은
|
||||
팝업, 모바일은 전체 페이지 리다이렉트를 씁니다. 모바일 브라우저는 팝업 UX 가 나쁘고
|
||||
문자 인증 같은 앱 전환이 얽히면 팝업 컨텍스트가 끊기기 쉽기 때문입니다.
|
||||
|
||||
kginicis 와 마찬가지로 이 플러그인은 실제 개인식별정보(PII)를 직접 보관하며, 결제 PG
|
||||
플러그인들과 달리 `sirsoft-ecommerce`를 비롯한 어떤 확장에도 의존하지 않습니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 주요 기능
|
||||
|
||||
<!-- @intent START -->
|
||||
| 영역 | 설명 |
|
||||
|---|---|
|
||||
| 본인확인 | NHN KCP 휴대폰 본인확인 팝업(데스크톱)/리다이렉트(모바일) |
|
||||
| 성인인증 | 만 19세 이상 여부만 확인하는 별도 purpose |
|
||||
| 중복가입 차단 | DI 또는 CI 기준으로 동일인 재가입 차단 (선택) |
|
||||
| 게스트 처리 | 비로그인 사용자의 본인확인 결과를 임시 보관 후 가입 완료 시 흡수 |
|
||||
| 개인정보 정리 | 사용자 탈퇴/삭제 시 보관 중인 PII 레코드 자동 파기 |
|
||||
| 마이페이지 | 본인확인 완료 상태 카드 노출 |
|
||||
| 관리자 설정 | 테스트/라이브 모드 전환, 중복가입 판정 기준 설정 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 동작 방식
|
||||
|
||||
<!-- @intent START -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[코어가 IDV 요구 · 428] --> B{기기 판별}
|
||||
B -->|데스크톱| C[사용자 클릭 → 팝업 오픈]
|
||||
B -->|모바일| D[복귀 정보 저장 → 페이지 리다이렉트]
|
||||
C --> E[KCP 인증 완료 → 팝업 종료 감지]
|
||||
D --> F[KCP 인증 완료 → 콜백 복귀]
|
||||
E --> G[결과 회수 → PII 확보]
|
||||
F --> G
|
||||
G -->|로그인 사용자| H[레코드 즉시 저장]
|
||||
G -->|비로그인 사용자| I[Cache 임시 보관 → 가입 완료 시 흡수]
|
||||
```
|
||||
|
||||
데스크톱 팝업은 반드시 사용자 클릭 이벤트 안에서 열립니다 — 비동기 콜백 이후에 열면
|
||||
브라우저 팝업 차단기에 걸립니다. 모바일 리다이렉트는 이 제약이 없는 대신, 복귀 후
|
||||
원래 상태를 되살리기 위한 정보를 브라우저에 임시 저장합니다.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 요구 사항
|
||||
|
||||
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| G7 코어 | `>=7.0.6` |
|
||||
| PHP | `^8.2` |
|
||||
<!-- @generated:requirements END -->
|
||||
|
||||
## 설치
|
||||
|
||||
<!-- @generated:install START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
```bash
|
||||
# 번들 설치 (코어에 동봉된 소스에서 설치)
|
||||
php artisan plugin:install sirsoft-verification_nhnkcp
|
||||
|
||||
# 활성화
|
||||
php artisan plugin:activate sirsoft-verification_nhnkcp
|
||||
|
||||
# 업데이트 (번들 소스 기준 강제 반영)
|
||||
php artisan plugin:update sirsoft-verification_nhnkcp --force
|
||||
```
|
||||
|
||||
저장소: https://github.com/gnuboard/g7-plugin-sirsoft-verification_nhnkcp
|
||||
<!-- @generated:install END -->
|
||||
|
||||
## 관리자 설정
|
||||
|
||||
<!-- @generated:settings-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 키 | 의미 | 기본값 |
|
||||
|---|---|---|
|
||||
| `is_test_mode` | 테스트 모드 | `true` |
|
||||
| `test_site_cd` | 테스트 사이트코드 | `AO7F3` |
|
||||
| `test_enc_key` | 테스트 암호화 키 | `c2a22fa3ebe4698075bcac6b433d52e351c881b02fb83488d4283a43385b1f8e` |
|
||||
| `live_site_cd` | 운영 사이트코드 | - |
|
||||
| `live_enc_key` | 운영 암호화 키 | - |
|
||||
| `web_siteid` | 웹사이트 ID | - |
|
||||
| `duplicate_field` | 중복 판정 필드 | `di` |
|
||||
| `duplicate_block_enabled` | 중복 가입 차단 | `true` |
|
||||
|
||||
개발자용 상세(타입·검증·저장 위치)는 [설정 스키마](docs/settings.md#설정-스키마) 를 보세요.
|
||||
<!-- @generated:settings-summary END -->
|
||||
|
||||
<!-- @intent START -->
|
||||
`live_site_cd`는 저장 시 `SM` 프리픽스가 자동으로 붙으므로 프리픽스 없이 입력해도 됩니다.
|
||||
`live_site_cd`/`live_enc_key`는 `is_test_mode`를 끄는(라이브 모드) 순간부터 필수가
|
||||
됩니다 — 테스트 모드에서는 비워둘 수 있습니다. `web_siteid`는 테스트/라이브 모드 공통으로
|
||||
쓰이는 웹사이트 식별자입니다. `duplicate_field`(`di` 또는 `ci`)와
|
||||
`duplicate_block_enabled`는 다른 IDV provider(예: `sirsoft-verification_kginicis`)와
|
||||
별개로 이 provider 를 통해 확인한 사용자에게만 적용됩니다. 라이브 암호화 키는 외부에
|
||||
노출하지 마세요.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 사용 방법
|
||||
|
||||
<!-- @intent START -->
|
||||
**활성화하기**: 플러그인을 활성화하면 자동으로 코어 IDV provider 목록에 등록됩니다.
|
||||
별도 화면 배치 작업 없이 코어가 이미 정의한 IDV 강제 지점(회원가입 등)에서 즉시
|
||||
동작합니다.
|
||||
|
||||
**중복가입 차단 켜기**: 동일인이 여러 계정을 만드는 것을 막고 싶다면
|
||||
`duplicate_block_enabled`를 켜고 `duplicate_field`로 DI/CI 중 판정 기준을 고릅니다.
|
||||
|
||||
**모바일 동작 확인**: 데스크톱에서는 정상인데 모바일에서만 인증이 실패한다면 리다이렉트
|
||||
복귀 경로(`redirectStash`)가 원인일 가능성이 높습니다 — §트러블슈팅을 확인하세요.
|
||||
|
||||
전체 API 목록은 [docs/api/](docs/api/README.md) 를, 발행/구독 훅 목록은
|
||||
[docs/extension-points.md](docs/extension-points.md) 를 참고하세요.
|
||||
<!-- @intent END -->
|
||||
|
||||
## 다른 확장과의 연동
|
||||
|
||||
<!-- @generated:integrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**이 확장이 의존하는 확장**
|
||||
|
||||
없음 — 코어만으로 동작합니다.
|
||||
|
||||
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
|
||||
|
||||
없음.
|
||||
<!-- @generated:integrations END -->
|
||||
|
||||
## 문서
|
||||
|
||||
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
|
||||
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
|
||||
| [docs/extension-points.md](docs/extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 | ✅ |
|
||||
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
|
||||
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
|
||||
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
|
||||
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
|
||||
<!-- @generated:docs-index END -->
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
<!-- @intent START -->
|
||||
| 증상 | 원인 | 조치 |
|
||||
|---|---|---|
|
||||
| 데스크톱에서 인증 버튼을 눌러도 팝업이 안 뜸 | 브라우저 팝업 차단기 | `startAuth`가 클릭 이벤트 핸들러 안에서 직접 호출되는지 확인 |
|
||||
| 모바일에서만 인증 완료 후 원래 화면으로 돌아오지 않음 | 리다이렉트 복귀 정보(`sessionStorage`의 `g7.identity.redirectStash`)가 유실됨 | 리다이렉트 도중 다른 탭/앱으로 완전히 전환되어 세션 스토리지가 초기화됐는지 확인 (동일 브라우저 탭 안에서 왕복해야 함) |
|
||||
| 설정 저장 시 422 오류 | 라이브 모드인데 `live_site_cd`/`live_enc_key` 미입력 | `is_test_mode`를 켜거나 라이브 자격증명을 입력 |
|
||||
| 이미 가입된 사용자인데 중복 오류 없이 재가입됨 | `duplicate_block_enabled`가 꺼져 있거나 `duplicate_field` 기준이 실제 판정과 다름 | 관리자 설정에서 두 값을 확인 |
|
||||
| 탈퇴한 사용자의 본인확인 정보가 남아있는 것으로 보임 | 정리 리스너 실행 여부를 별도로 확인하지 않음 | `CleanKcpRecordOnUserWithdraw`/`CleanKcpRecordOnUserDelete` 정상 등록 여부를 훅 캐시에서 확인 |
|
||||
<!-- @intent END -->
|
||||
|
||||
## 변경 이력
|
||||
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,22 @@
|
||||
# NHN KCP 휴대폰 본인확인 개발자 문서
|
||||
|
||||
> plugins/_bundled/sirsoft-verification_nhnkcp · 플러그인
|
||||
|
||||
<!-- @generated:stats START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
**훅 수**: 0 · **구독 훅 수**: 7 · **라우트 수**: 2 · **모델 수**: 2 · **테이블 수**: 2 · **마이그레이션 수**: 2 · **레이아웃 수**: 1 · **핸들러 수**: 1
|
||||
<!-- @generated:stats END -->
|
||||
|
||||
## 문서 목차
|
||||
|
||||
<!-- @generated:doc-toc START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | 설계 의도·계층 지도·디렉토리 맵 |
|
||||
| [extension-points.md](extension-points.md) | 발행/구독 훅·미들웨어·채널·스케줄 |
|
||||
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
|
||||
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
|
||||
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
|
||||
| [api/](api/README.md) | API 레퍼런스 |
|
||||
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
|
||||
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
|
||||
<!-- @generated:doc-toc END -->
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user