KISA 제보 3건(KVE-2026-2010/2011/2018)과 그 동일 계열 형제 결함을 전수 조치하고, 그 과정에서 드러난 두 결함군을 함께 닫는다. - 검증 시점과 연결 시점이 host 를 다르게 읽던 SSRF 통로를 정규화 SSoT 한 곳으로 모았다 - 세션을 여는 지점(2FA 완료·토큰 재발급)이 잠금 검사를 거치지 않아 계정 잠금이 우회됐다 - 인증도 서명도 없는 브라우저 리턴 콜백이 주문 상태를 바꾸던 통로를 4 PG 전부에서 닫고, 소유권을 대조하는 close-report 를 토스에도 신설했다. 그 결과 정리 주체를 잃는 결제창 미완료 주문은 만료 자동취소가 거둔다 - 저장소 A(_local)에만 쓰는 경로가 B 의 값을 조용히 덮던 회귀를 정본 writer 로 닫았다 (engine-v1.63.5). 한 방향만 보던 정적 검사에 반대 방향 축과 양방향 계약 테스트를 더했다 - 레이아웃 JSON 의 같은 객체 중복 키가 앞선 선언을 오류 없이 삼키던 결함군을 닫았다
12 KiB
이커머스 — 설정·권한·라우트
설정 스키마·권한·메뉴·라우트·의존 관계 · 진입점: AGENTS.md
설정 스키마
getSettingsSchema() 선언이 없습니다.
기본값 파일: config/settings/defaults.json
getSettingsSchema() 선언이 없는 것은 누락이 아닙니다. 이 모듈의 설정은 코드가 아니라
config/settings/defaults.json 이 SSoT 이며, 그 파일 하나가 세 가지를 함께 담습니다:
| 키 | 역할 |
|---|---|
_meta.categories |
설정 그룹 9개의 목록과 순서 (basic_info · language_currency · order_settings · shipping · seo · review_settings · inquiry · notifications · mileage) |
defaults |
그룹별 기본값. 설치 시 storage/app/settings/ 로 동기화되어 module_setting() 이 읽는 값이 됩니다 |
frontend_schema |
관리자 화면이 자동으로 그리는 입력 폼 정의 |
frontend_schema 는 8개 그룹뿐이고 mileage 가 없습니다. 자동 생성 폼으로는 표현할 수 없는
입력(통화별 적립 규칙 표 등)이 있어서 마일리지 탭만 레이아웃 JSON 으로 직접 그리기 때문입니다
(resources/layouts/admin/partials/admin_ecommerce_settings/_tab_mileage*.json 6개). 새 그룹을
추가할 때는 이 셋 중 어디까지 손댈지를 먼저 정합니다 — _meta.categories 에만 넣고 defaults
를 빠뜨리면 그 그룹은 화면에 뜨지만 저장할 값이 없습니다.
설정 값을 코드에서 읽을 때는 EcommerceSettingsService 를 거칩니다. 통화 설정처럼 요청마다
여러 번 읽히는 값은 CurrencySettingsCache 가 따로 캐시하며, 설정 저장 후 캐시를 비우는 것은
core.module_settings.after_save 훅을 받는 리스너들입니다 — 설정을 직접 파일에서 읽으면 그
무효화 경로를 타지 않아 화면과 서버가 서로 다른 값을 봅니다.
결제수단 목록(order_settings.payment_methods)만은 성격이 다릅니다. 저장값과 플러그인이
등록한 카탈로그의 병합이라, 플러그인을 삭제·비활성화하면 저장값은 남아 있는데 카탈로그에서
사라지는 고아 항목이 생깁니다. 공개 응답은 고아 항목을 걸러 내보내고 관리자 응답은 그대로
노출하는 것이 규칙입니다 — 운영자는 그것을 보고 지워야 하기 때문입니다.
order_settings.pending_order_expire_minutes 는 결제창까지 갔으나 결제가 성립하지 않은
주문(PENDING_ORDER)을 만료 자동취소 대상에 넣는 기준입니다(기본 1440분 = 24시간). 0 이면 그
부류를 정리하지 않습니다.
이 값은 환경설정 > 주문 설정 > 주문 자동취소 카드에 있습니다. 형제 값
auto_cancel_days 와 달리 auto_cancel_expired 토글 하위가 아닙니다 — 정리 스케줄러가 그
토글과 무관하게 이 값을 읽기 때문에, 토글을 끈 상태에서 이 입력만 사라지면 화면이 실제 동작을
설명하지 못합니다. 입력 경계(0 ~ 20160분)는 config/ecommerce.php 의 limits 가 단일 출처이며,
화면은 설정 응답의 _meta.limits 로 같은 값을 받습니다.
이 부류를 정리 대상에 넣은 이유는 정리 주체가 아예 없었기 때문입니다. 기존 자동취소는 입금
기한(vbank_due_at·deposit_due_at)이 있는 결제수단만 훑고, 임시주문 정리는 다른 테이블을 봅니다.
브라우저 리턴 콜백이 주문 상태를 바꾸지 않게 된 뒤로는 승인 거절분도 여기에 머무르므로, 이 정리가
선차감 마일리지 복원의 최종 안전망입니다.
권한
| 카테고리 | 이름 | 액션 | 라우트 키 |
|---|---|---|---|
products |
상품 관리 | read, create, update, delete |
product |
orders |
주문 관리 | read, update |
order |
categories |
카테고리 관리 | read, create, update, delete |
- |
brands |
브랜드 관리 | read, create, update, delete |
brand |
product-notice-templates |
상품정보제공고시 관리 | read, create, update, delete |
- |
product-common-infos |
공통정보 관리 | read, create, update, delete |
- |
settings |
환경설정 | read, update |
- |
promotion-coupon |
쿠폰 관리 | read, create, update, delete |
coupon |
shipping-policies |
배송정책 관리 | read, create, update, delete |
shippingPolicy |
product-labels |
상품 라벨 관리 | read, create, update, delete |
- |
identity.policies |
이커머스 본인인증 정책 | read, update |
- |
reviews |
리뷰 관리 | read, update, delete |
review |
inquiries |
문의 관리 | update, delete |
- |
dashboard |
대시보드 | view |
- |
user-products |
사용자 상품 | read |
- |
user-orders |
사용자 주문 | create, cancel, confirm |
- |
user-reviews |
사용자 리뷰 | write |
- |
mileage |
마일리지 관리 | read, manage |
mileage-transaction |
user-currency |
회원 결제 통화 관리 | manage |
- |
user-shipping-country |
회원 배송국가 관리 | manage |
- |
권한 20종은 세 무리로 갈립니다.
- 관리자 CRUD 12종 (
products·orders·categories·brands·product-notice-templates·product-common-infos·promotion-coupon·shipping-policies·product-labels·reviews·inquiries·mileage): 관리자 화면과 1:1 대응하며, 라우트 키가 있는 것은 그 라우트에 스코프 미들웨어가 걸립니다. - 사용자 측 5종 (
user-products·user-orders·user-reviews·user-currency·user-shipping-country): 구매자가 자기 자원에 대해 갖는 권한입니다. 관리자 권한과 이름이 겹치지 않도록user-접두사를 씁니다. - 횡단 3종 (
settings·dashboard·identity.policies): 화면 하나에 대응합니다.
orders 에 create/delete 가 없는 것은 의도입니다 — 주문은 구매자 결제로 생기고
(user-orders.create), 삭제 대신 취소·환불로 처리합니다. inquiries 에 read 가 없는 것도
같은 성격입니다: 문의 본문은 게시판 모듈이 소유하므로 읽기 권한은 그 게시판의 권한이
정하고, 이 모듈은 답변·삭제만 관장합니다.
새 관리자 화면을 추가하면 권한·메뉴·라우트 이름 셋이 서로를 정확히 가리키는지 확인합니다. 권한만 추가하고 메뉴를 빠뜨리면 화면에 도달할 길이 없고, 반대면 눌러도 403 입니다.
메뉴
| 구분 | slug | 이름 | URL | 하위 |
|---|---|---|---|---|
| 관리자 | sirsoft-ecommerce |
이커머스 | - | 11개 |
최상위 sirsoft-ecommerce 아래 11개 하위 메뉴가 붙습니다 — 환경설정 · 상품 · 카테고리 ·
브랜드 · 상품정보제공고시 · 공통정보 · 주문 · 쿠폰 · 배송정책 · 리뷰 · 마일리지 내역.
메뉴는 권한과 짝을 이룰 때만 보입니다. 운영자에게 역할이 부여되어도 그 역할에 해당 권한이
없으면 메뉴가 렌더되지 않으므로, 새 화면을 추가할 때는 getPermissions() 와 getAdminMenus()
를 함께 바꿉니다.
권한 표에는 있는데 메뉴가 없는 것들(dashboard · identity.policies · product-labels 등)은
독립 메뉴가 아니라 다른 화면 안에 들어 있기 때문입니다 — 대시보드는 코어 관리자 첫 화면에
레이아웃 조각으로 주입되고, 본인인증 정책은 코어 IDV 설정 화면에서 함께 다뤄지며, 상품 라벨은
상품 관리 화면 안에 있습니다.
라우트
| 종류 | 파일 | URL prefix |
|---|---|---|
api |
src/routes/api.php |
/api/modules/sirsoft-ecommerce/... |
확장 라우트는 활성 상태인 확장의 것만 등록됩니다. 라우트 정의를 바꾸면 라우트 캐시 재생성이 필요합니다.
라우트 239개가 파일 하나(src/routes/api.php)에 모여 있고 전부 /api/modules/sirsoft-ecommerce/
아래로 나갑니다. 화면용 라우트는 없습니다 — 관리자 화면은 레이아웃 JSON 이 이 API 를 호출해
그리고, 방문자 화면은 템플릿이 같은 API 를 씁니다.
대상별로 셋으로 갈립니다:
| 무리 | 인증 | 비고 |
|---|---|---|
admin.* |
관리자 인증 + 권한 스코프 | 라우트 키가 선언된 권한이 여기에 걸립니다 |
user.* · 공개 조회 |
Sanctum(일부는 optional.sanctum) |
비로그인도 상품·카테고리는 봅니다 |
guest.orders.* |
VerifyGuestOrderToken |
비회원 주문 조회·취소. 토큰이 곧 신원이므로 새 라우트를 추가하면 미들웨어 선언(getMiddleware())에도 그 이름을 반드시 추가합니다 |
라우트를 추가·변경한 뒤에는 라우트 캐시를 다시 구워야 합니다. 확장 라우트는 활성 상태인 확장의 것만 등록되고, 캐시에 없는 라우트는 예외도 경고도 없이 그대로 404 가 됩니다.
모든 라우트에 name() 이 필요합니다 — 이름이 없으면 미들웨어 self-gate 의 targets 패턴과
IDV 정책의 라우트명 인덱스가 그 라우트를 찾지 못해, 보호가 걸린 것처럼 보이지만 실제로는
통과합니다.
의존 관계
이 확장이 의존하는 확장
없음 — 코어만으로 동작합니다.
이 확장에 의존하는 확장 (이 확장을 비활성화하면 함께 영향을 받습니다)
| 확장 | 유형 | 요구 버전 |
|---|---|---|
sirsoft-pay_kginicis |
플러그인 | >=1.1.0 |
sirsoft-pay_nhnkcp |
플러그인 | >=1.1.0 |
sirsoft-pay_nicepayments |
플러그인 | >=1.1.0 |
sirsoft-tosspayments |
플러그인 | >=1.1.0 |
sirsoft-basic |
템플릿 | >=1.1.0 |
이 모듈은 아무 확장에도 의존하지 않습니다. 코어만 있으면 동작하며, 관계는 전부 한 방향으로
들어옵니다 — 결제 플러그인 4종과 템플릿 sirsoft-basic 이 이 모듈을 요구합니다.
그 방향이 뒤집히지 않게 유지하는 것이 이 모듈 설계의 핵심입니다. PG 이름을 이 모듈 코드에 넣는 순간 의존이 양방향이 되고, 새 PG 를 붙일 때마다 이 모듈을 고쳐야 합니다.
manifest 에는 없지만 실제로 맞물리는 확장이 둘 더 있습니다:
| 확장 | 무엇으로 연결되는가 | 없으면 |
|---|---|---|
sirsoft-board |
훅 3종 구독 (문의 글 삭제·복원·일괄삭제 시 피벗 정리) + 설정 inquiry.board_slug |
상품 문의 기능만 비고 나머지는 정상 |
sirsoft-ckeditor5 |
훅 1종 (편집기가 참조할 이커머스 리소스 목록 제공) | 편집기에서 상품 링크를 고를 수 없을 뿐 |
훅 구독은 상대가 없으면 발화하지 않으므로 이 둘을 manifest 의존으로 올리지 않는 것이 맞습니다.
다만 그 대가로 상대 확장이 훅 이름을 바꾸면 예외 없이 조용히 연동이 끊깁니다 — 상대의
docs/extension-points.md 를 함께 확인해야 하는 이유입니다.
이 모듈의 공개 표면(Service·Repository·Contracts·라우트·발행 훅)을 바꿀 때는 위 4+1 개 확장의
dependencies 최소 버전 상향이 필요한지 검토합니다.