구현 구조
데모는 외부 의존성 없이 PHP 8.3 + MariaDB 로 만들었습니다. 프레임워크도 패키지 매니저도 쓰지 않아 배포에 빌드 단계가 없고, 규격이 어디 구현됐는지 파일 하나로 찾아갈 수 있습니다.
기획 패키지에서 숫자나 판정 규칙이 적힌 모든 지점은 한 파일에만 구현했습니다.
| 기획 문서 | 구현 위치 | 핵심 내용 |
|---|---|---|
| 04번 6장 — 영업 중 판정 | src/Services/OpenStateResolver.php |
임시휴무 → 수동 토글 → 요일별 시간 → 브레이크 순 우선순위. 자정 넘김, 경계 시각(시작 포함·종료 미포함), 격주 휴무, 다음 영업 시각 탐색 |
| 06번 2~4장 — 추천 산식 | src/Services/FeedRanker.php |
D·O·F·T·A·N 가중합 + 패널티, 제외 6종, 다양성 재배치(점수 불변), 동점 처리 고정, 추천 사유 생성 |
| 07번 4장 — 위치 조회 | src/Services/GeoQuery.php |
바운딩 박스(lat ± R/111320, lng ± R/(111320·cos lat)) → 하버사인 → 행정동 합집합 중복 제거 |
| 05번 5장 — 출력 검증 | src/Services/AiCopywriter.php::validate() |
7단계 검사표. 재요청 대상(1·2·6)과 후보 제외 대상(3·4·5)을 구분 |
| 05번 3장 — 어댑터 | src/Services/Llm/ |
LlmAdapter 인터페이스 + MockAdapter(데모) + GeminiAdapter(실물 구현). 설정 한 줄로 교체 |
| 05번 8장 — 비용 3겹 | AiCopywriter::generate() |
캐시 키 = SHA-256(사진 해시 + 정렬 키워드 + 톤 + 프롬프트 버전) → 매장별 쿼터 → 전체 상한 |
| 05번 6장 — 금칙어 | src/Services/WordFilter.php |
AI 후보와 직접 작성 글 양쪽에 적용. 직접 작성은 차단하지 않고 검토 대상 표시 |
| 04번 8장 — 푸시 멱등 | src/Controllers/OwnerApi.php::dispatchPush() |
post:{id}:user:{id} UNIQUE 충돌로 중복 발송 차단 · 야간 보류 |
| 06번 9-2장 — 미리보기 | src/Controllers/AdminApi.php::weightsPreview() |
현재 가중치와 미저장 가중치로 피드를 두 번 만들어 순위 차이 계산 |
| 03번 1-7 / 2-5 / 3-7장 | public/assets/*.js |
와이어프레임의 요소 배치를 그대로 구현. 화면 ID를 주석과 해설 패널에 표기 |
제안서의 실제 구현은 Laravel입니다 (07번 2장). 이 데모만 의존성 0으로 만든 이유는 하나입니다 —
심사자가 서버에서 바로 열어볼 수 있어야 하고, composer install이 실패할 여지를 없애야 했습니다.
레이어 구분(Controller / Service / Db)은 Laravel 로 옮겨도 그대로 유지됩니다.
목록·피드는 매장마다 쿼리를 날리지 않습니다. 영업시간·임시휴무·수동토글·최신 소식을
매장 ID 묶음으로 한 번에 읽고 메모리에서 합칩니다 (FeedRanker::load*).
점수 계산도 SQL이 아니라 애플리케이션에서 합니다 — 가중치를 바꿔도 쿼리를 고치지 않습니다.
store.last_post_at · view_count · favorite_count ·
report_count는 원본에서 셀 수 있지만 일부러 중복 저장했습니다.
추천이 매장마다 소식을 세면 느려지기 때문입니다. 대신 어긋날 수 있다는 전제로
집계 배치가 하루 1회 대조합니다 (04번 5장).
피드에 무작위 요소가 없습니다. 같은 좌표·시각·이용자면 항상 같은 순서가 나옵니다.
동점은 last_post_at DESC → id ASC로 고정합니다.
"내 가게가 안 보인다"는 사장님 문의를 조사할 수 있어야 하기 때문입니다 (06번 7장).
| 경로 | 줄 수 |
|---|---|
public/assets/app.css | 1,491 |
public/assets/admin.js | 877 |
public/assets/customer.js | 846 |
src/Controllers/AdminApi.php | 795 |
db/seed.php | 769 |
src/Controllers/CustomerApi.php | 614 |
public/assets/owner.js | 600 |
src/Controllers/OwnerApi.php | 536 |
src/Services/AiCopywriter.php | 534 |
db/schema.sql | 510 |
db/make_images.php | 491 |
src/Services/FeedRanker.php | 452 |
src/Services/Llm/MockAdapter.php | 269 |
src/Services/OpenStateResolver.php | 203 |
src/Services/GeoQuery.php | 146 |
src/Services/Llm/GeminiAdapter.php | 98 |
src/Services/WordFilter.php | 95 |
public/assets/shared.js | 92 |
src/Db.php | 80 |
src/Router.php | 56 |
04번 문서의 스키마를 교정본으로 생성했습니다. 2차 결제 확장 예약 테이블은 만들어 두고 쓰지 않습니다.
| 테이블 | 용도 |
|---|---|
admin_log | 관리자 활동 로그 |
admin_user | 관리자 계정 (운영자·검수자) |
ai_generation | AI 호출 기록 · 비용·품질 추적 |
app_config | 앱 설정 (쿼터·상한·야간 정책) |
block | 차단 |
category | 업종 + 시간대 가중치 표 (06번 3-4장) |
daily_stat | 일별 집계 — 통계 화면 전용 |
favorite | 관심 매장 + 매장별 알림 on/off |
feed_event | 피드 노출·클릭·저장 + 가중치 버전 (교정) |
feed_weight | 추천 가중치 버전 |
filter_catch | 필터 적발 내역 · 오탐 판정 |
menu_item | 메뉴·상품 |
notification | 알림함 |
order | 2차 결제 예약 — 1차 미사용 |
order_item | 2차 결제 예약 — 가격 스냅샷 |
ow30_measurement | 30초 등록 측정 — 완료 기준 1번 |
owner_application | 심사 신청 · 본인인증 · 국세청 조회 원응답 |
payment | 2차 결제 예약 — PG 결합 지점 |
post | 소식. source 가 무수정 게시율의 원천 |
post_image | 소식 사진 |
post_tag | 야간 배치 추출 태그 |
push_dispatch | 푸시 발송. 멱등 키 유니크 |
region | 행정동 기준 정보 · 중심점 · 데모 지도 경계 |
report | 신고. (신고자, 대상) 유니크로 중복 신고 차단 |
store | 매장. 승인 상태 전용 유니크로 중복 승인 차단 (교정) |
store_closure | 임시 휴무 일자 |
store_hour | 요일별 영업시간 + 격주 휴무 week_parity (교정) |
store_image | 매장 사진 |
store_open_state | 수동 토글 + 자동 해제 시각 |
user | 회원. role ENUM 대신 is_owner 플래그 (교정) |
user_region | 저장한 동네 최대 3개 |
view_log | 조회 로그. 복합 PK + 일 단위 파티션 (교정) |
word_filter | 금칙어·과장 표현 사전 |