구현 구조

기획서의 어느 줄이 코드의 어느 파일인지

데모는 외부 의존성 없이 PHP 8.3 + MariaDB 로 만들었습니다. 프레임워크도 패키지 매니저도 쓰지 않아 배포에 빌드 단계가 없고, 규격이 어디 구현됐는지 파일 하나로 찾아갈 수 있습니다.

소스 9,668줄 테이블 33개 외부 패키지 0개 PHP 8.3.33

규격 → 코드 매핑

기획 패키지에서 숫자나 판정 규칙이 적힌 모든 지점은 한 파일에만 구현했습니다.

기획 문서구현 위치핵심 내용
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 로 옮겨도 그대로 유지됩니다.

N+1 을 만들지 않는 지점

목록·피드는 매장마다 쿼리를 날리지 않습니다. 영업시간·임시휴무·수동토글·최신 소식을 매장 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.css1,491
public/assets/admin.js877
public/assets/customer.js846
src/Controllers/AdminApi.php795
db/seed.php769
src/Controllers/CustomerApi.php614
public/assets/owner.js600
src/Controllers/OwnerApi.php536
src/Services/AiCopywriter.php534
db/schema.sql510
db/make_images.php491
src/Services/FeedRanker.php452
src/Services/Llm/MockAdapter.php269
src/Services/OpenStateResolver.php203
src/Services/GeoQuery.php146
src/Services/Llm/GeminiAdapter.php98
src/Services/WordFilter.php95
public/assets/shared.js92
src/Db.php80
src/Router.php56

테이블

04번 문서의 스키마를 교정본으로 생성했습니다. 2차 결제 확장 예약 테이블은 만들어 두고 쓰지 않습니다.

테이블용도
admin_log관리자 활동 로그
admin_user관리자 계정 (운영자·검수자)
ai_generationAI 호출 기록 · 비용·품질 추적
app_config앱 설정 (쿼터·상한·야간 정책)
block차단
category업종 + 시간대 가중치 표 (06번 3-4장)
daily_stat일별 집계 — 통계 화면 전용
favorite관심 매장 + 매장별 알림 on/off
feed_event피드 노출·클릭·저장 + 가중치 버전 (교정)
feed_weight추천 가중치 버전
filter_catch필터 적발 내역 · 오탐 판정
menu_item메뉴·상품
notification알림함
order2차 결제 예약 — 1차 미사용
order_item2차 결제 예약 — 가격 스냅샷
ow30_measurement30초 등록 측정 — 완료 기준 1번
owner_application심사 신청 · 본인인증 · 국세청 조회 원응답
payment2차 결제 예약 — 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금칙어·과장 표현 사전