Skip to Content
정비사업 매칭 (광고주 제품)

정비사업 매칭 (광고주 제품)

정비사업 집행부와 검증된 시공사·신탁사·설계사를 잇는 1:1 매칭입니다. 광고주가 단지를 검색해 관심(찜)을 보내고, 집행부가 수락하면 양쪽 연락처가 상호 공개됩니다.

매칭의 종착점은 연락처 교환입니다. PRD의 인앱 1:1 DM은 아직 구현돼 있지 않습니다. “DM 매칭”이라는 표현이 기획 문서에 남아 있어도 현재 코드에는 채팅 화면이 없습니다.

전체 흐름

[광고주] 입점 신청 → (운영팀 수동 승인) → 로그인 → 단지 검색 → 찜 발송(월 한도 차감) [집행부] 받은 제안 확인 → 수락 / 거절(사유 필수) 수락 시에만 양쪽 연락처 상호 공개

승인 UI는 이 레포에 없습니다 — 운영팀이 백오피스에서 AdvertiserStatusREQUESTED → APPROVED로 바꿉니다.

화면

전부 apps/dashboard 안에 있습니다. apps/app·native-app에는 매칭 코드가 없습니다.

광고주 포털 — /advertiser (조합 [unionId] 밖 독립 트리)

경로화면인증
/advertiser/apply입점 신청 폼공개
/advertiser/pending승인 대기 안내공개
/advertiser단지 검색게이트 안
/advertiser/interests내 찜 목록게이트 안
/advertiser/me내 정보게이트 안

셸은 대시보드 사이드바가 아니라 자체 top-nav(AdvertiserHeader)입니다. 외부 사용자용 별도 제품 표면이라 의도적으로 다릅니다. 페이지 폭은 AdvertiserPageContainerwide(960) / narrow(640) 두 단계만 씁니다.

집행부 — /[unionId]/matching-proposals

받은 제안 카드 목록 + 수락·거절·연락처 다이얼로그. 셸은 다른 대시보드 화면과 동일한 규칙 (h-12 헤더 + px-3 pb-3 안의 rounded-2xl 카드, 스크롤은 카드 내부).

사이드메뉴 노출은 아직 하드코딩된 조합 ID 하나로만 게이팅됩니다 (SideMenuItems.tsxAD_MATCHING_ALLOWED_UNION_ID). 다른 조합에서 테스트하려면 이 값을 봐야 합니다.

데이터 경계 — 가장 중요한 계약

packages/domains/types/matching.ts에 타입 주석으로 강제돼 있습니다. 어기면 개인정보 유출입니다.

타입노출 시점담는 것
RegionMatchingProposal수락 전에도 상시회사명·유형·주소·회사 실적·메시지
MatchedAdvertiserContact수락 후에만담당자 실명·부서·직책·연락처·개인 실적

담당자 정보를 RegionMatchingProposal에 절대 추가하지 마세요. 서버도 미수락 건에는 MATCH_NOT_ACCEPTED(403)로 차단합니다.

API

packages/api/lib/matching/{index.ts,schema.ts} — 두 클래스로 갈립니다.

  • MatchingPartnerAPI (광고주, v1/partner/*) — 9개 메서드
  • DashboardMatchingAPI (집행부, v1/dashboard/region/{regionId}/matching-proposals*) — 4개

DB 스키마는 이 레포에 없습니다. 영속화는 백엔드(~/workspace/howmuchhome, FastAPI) 소유이고 프론트는 타입 + REST 클라이언트만 가집니다.

공유 컴포넌트

apps/dashboard/src/features/advertiser/ — 두 라우트 트리가 갈라져 있어 같은 값을 각자 정의하던 것을 모았습니다.

파일용도
constants.ts광고주 유형·제안 상태·사업유형 라벨의 단일 소스
MatchingStatusTag / AdvertiserTypeBadge상태·유형 배지
MatchingEmptyState빈 상태
MatchingLoadingSkeleton로딩 스켈레톤 (aria-busy 밖에 role="status")
DescriptionList연락처·프로필 <dl>
ReferenceList정비사업 실적 목록
formatMatchingDateKST 기준 YYYY.MM.DD
matchingErrorMessage에러코드 → 한국어 문구

함정

전부 실제로 밟았거나 리뷰에서 잡힌 것입니다.

1. 에러 메시지를 그대로 토스트에 띄우지 말 것

ApiServerError.message`${errorCode} - ${detail}` 형식입니다(packages/api/error.ts). 서버 detail이 비면 사용자에게 MATCHING_PROPOSAL_QUOTA_EXCEEDED가 그대로 보입니다. getMatchingErrorMessage(error, fallback)을 쓰세요.

2. 날짜를 createdAt.slice(0, 10)으로 자르지 말 것

UTC 날짜가 그대로 나와서 한국시간 자정 직후(UTC 전날 15시 이후)에 보낸 제안이 하루 전날로 보입니다. formatMatchingDate를 쓰세요.

3. 상태 필터는 백엔드 지원이 없음

GetRegionProposalsRequestregionId/limit/offset만 받습니다. status 파라미터가 없어서 클라이언트에서 거르면 현재 페이지 안에서만 걸러지고 탭 건수도 틀리게 나옵니다. 상태 필터가 필요하면 백엔드에 쿼리 지원을 먼저 요청하세요.

4. 페이지네이션은 범위 이탈을 보정할 것

제안이 철회되면 total이 줄어 보고 있던 마지막 페이지가 범위를 벗어납니다. 빈 상태를 items.length === 0으로 판정하면 페이지네이션 바까지 사라져 빠져나올 수 없습니다. total === 0일 때만 빈 상태를 띄우고, 범위를 벗어나면 마지막 유효 페이지로 되돌리세요.

5. RSC prefetch와 클라이언트 초기 상태의 limit/offset을 맞출 것

어긋나면 하이드레이션 직후 같은 데이터를 한 번 더 요청합니다. 기본 페이지 크기를 _lib/pagination.ts로 양쪽이 공유합니다.

6. 광고주 로그인은 공유 /sign-in 폴백

별도 로그인 페이지가 없습니다. dashboardSignIn 먼저 → 4xx면 MatchingPartnerAPI.login 폴백 (app/(auth)/sign-in/actions.tsx). getMe의 403은 “미승인 광고주”와 “비광고주”를 구분하지 못하므로, 어디로 보낼지는 어느 로그인이 성공했는지(loginType)로 판별합니다 (resolvePostLoginPath).

영업용 데모 — /demo

로그인 없이 광고주 시점 ↔ 집행부 시점을 오가며 전 시나리오를 시연하는 공개 라우트입니다. apps/dashboard/src/app/demo/.

매칭 화면 컴포넌트의 로직을 복제하지 않고 재사용합니다. configureApiFetcher (packages/api/lib/HowmuchhomeAPI.ts)로 브라우저 fetcher만 인메모리 스토어로 교체하는 방식입니다. @MutationOptions의 invalidate 체인이 살아 있어 찜을 보내면 quota 배지가 실제로 줄어듭니다.

데모가 프로덕션과 다르게 보이는 부분 (HMH-9294)

고객이 “이렇게 바뀌면 좋겠다”는 안을 보고용으로 요청하면, 프로덕션을 먼저 바꾸지 않고 데모에서만 그 모습을 보여줍니다. 지금 어긋나 있는 건 둘입니다.

데모프로덕션어떻게
사이드바·헤더·탭이 “내 명함첩""매칭 제안” 그대로_client/labels.tsDEMO_INBOX_MENU_TITLE 한 곳에서만 정의
제안 카드가 명함 모양(DemoBizCard)MatchingProposalCard 그대로MatchingProposalListSectionpresentation prop

전문가 브라우징 메뉴(“전문가 명함첩”, DEMO_EXPERT_DIRECTORY_MENU_TITLE)는 프로덕션에 아직 없는 신규 기능이라 위 표의 “어긋남”과는 다른 종류입니다. 두 이름은 남의 명함을 모아 둔 곳 ↔ 나에게 온 명함으로 짝지어 읽히도록 함께 정합니다 (HMH-9382) — 한쪽만 바꾸면 대비가 깨집니다. 메뉴를 누르면 카테고리를 고르는 화면 없이 전문가 전체 목록이 바로 열리고, 카테고리는 목록 상단 칩(전문가 수 포함)으로 좁힙니다. 광고 배너는 그 목록 상단에 있고 칩 선택을 따라갑니다.

presentation문구와 카드 컴포넌트만 주입받는 선택적 prop입니다. 페이지네이션·다이얼로그· 범위 보정 로직은 프로덕션 섹션에 그대로 두고, 넘기지 않으면 프로덕션 기본값이 쓰입니다 — 집행부 화면의 동작·외형은 이 prop이 생기기 전과 같습니다. 데모가 목록 섹션을 통째로 복제하면 안 되는 이유가 이겁니다: 복제본은 프로덕션이 고쳐질 때 조용히 뒤처집니다.

담기는 정보는 두 카드가 같아야 합니다. 명함 카드에서 항목을 빼면 “수락 전에는 회사 정보만 보인다”는 데모의 핵심 설명이 화면으로 증명되지 않습니다.

광고 배너 크리에이티브 — ?ad=

집행부 화면의 광고 지면은 목업입니다(노출·클릭 집계나 소재 입고 경로가 없음). 소재는 ?ad= 쿼리로 갈아끼웁니다 — _mock/fixtures.tsDEMO_AD_CREATIVES.

URL소재
/demo기본. 가상 광고주(얼마집종합건설), 브랜드 로고 없음
/demo?ad=imseciM증권 로고·키컬러·카피

특정 광고주에게 자사 로고로 된 지면 그림을 보여줘야 할 때 쓰는 장치입니다. 기본 /demo는 가상 회사로 남겨두세요 — 다른 광고주 상담 자리에서 “저 회사는 이미 쓰고 있네”라는 오해가 생깁니다. 실존 업체명은 fixture 규칙상 금지이고, imsec이 그 유일한 예외입니다(사유는 DEMO_AD_CREATIVES.imsec 주석).

브랜드 소재를 추가할 때: 카피는 광고주가 보낸 문구를 그대로 씁니다(보고서에 그대로 들어감). 광고 표기와 지면 규격 캡션은 컴포넌트에 박혀 있어 뺄 수 없습니다 — 표시광고법상 필요하고, 목업에서 빼두면 실제 구현 때 “원래 없었잖아요”가 되기 쉽습니다.

로고는 ImSecLogo처럼 컴포넌트 하나로 분리해 두세요. 공식 파일을 나중에 받으면 거기만 갈아끼우면 됩니다.

데모를 만질 때 반드시 지킬 것

장치이유
dynamic(ssr: false)RSC가 렌더하면 서버엔 데모 fetcher가 없어 Next 서버가 실제 API를 호출한다
첫 렌더 전 설치 + effect에도 멱등 설치StrictMode가 mount→unmount→remount 하며 cleanup으로 fetcher를 지운다. 재설치가 없으면 이후 요청이 전부 실제 API로 샌다 (실제로 밟음)
fail-closed 핸들러등록되지 않은 경로는 throw. 네트워크로 나가는 경로가 구조적으로 없게 만든다
격리 CookieJarAdvertiserSignOutButton이 실제 accesstoken을 지운다. 안 막으면 데모 중 로그아웃한 사람이 실제 세션을 잃는다
handlers.test.ts핸들러 경로가 packages/api와 중복이라, __apiMetadata.pathFn으로 실제 경로를 만들어 대조한다. 엔드포인트가 바뀌면 조용히 죽지 않고 CI에서 깨진다

데모를 수정했으면 프로덕션 빌드에서 전 시나리오를 돌린 뒤 DevTools Network에 v1/partner·v1/dashboard 요청이 0건인지 확인하세요.

관련 티켓

기반: HMH-7738(Phase A~C) · HMH-7740(Phase D) · HMH-7756(집행부 화면) · HMH-7838/7847/7852(로그인) · HMH-8516(등록정보 확장) · HMH-8723(유형 5종) 디자인 정리: HMH-8900(공유 프리미티브) · HMH-8907(집행부) · HMH-8909(광고주 포털) · HMH-8913(데모) 데모 보고용 변형: HMH-9294(iM증권 배너 크리에이티브 · 명함 카드 · 매칭 제안 → “명함첩” 이름 변경안) 전문가 브라우징: HMH-9341(카테고리 브라우징 · 역방향 명함교환) · HMH-9382(“전문가 명함첩”/“내 명함첩” 이름 조합 · 목록 직접 진입)

Last updated on