influencer-requests.vue 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477
  1. <template>
  2. <div>
  3. <div class="inner--headers">
  4. <h2>{{ pageId }}</h2>
  5. <div class="bread--crumbs--wrap">
  6. <span>홈</span>
  7. <span>벤더 대시보드</span>
  8. <span>{{ pageId }}</span>
  9. </div>
  10. </div>
  11. <!-- 통계 카드 -->
  12. <div class="stats--cards--wrap">
  13. <div class="stats--card">
  14. <div class="stats--icon pending">
  15. <v-icon>mdi-clock-outline</v-icon>
  16. </div>
  17. <div class="stats--content">
  18. <h3>{{ stats.pending || 0 }}</h3>
  19. <p>대기 중인 승인요청</p>
  20. </div>
  21. </div>
  22. <div class="stats--card">
  23. <div class="stats--icon approved">
  24. <v-icon>mdi-check-circle</v-icon>
  25. </div>
  26. <div class="stats--content">
  27. <h3>{{ stats.approved || 0 }}</h3>
  28. <p>승인 완료</p>
  29. </div>
  30. </div>
  31. <div class="stats--card">
  32. <div class="stats--icon rejected">
  33. <v-icon>mdi-close-circle</v-icon>
  34. </div>
  35. <div class="stats--content">
  36. <h3>{{ stats.rejected || 0 }}</h3>
  37. <p>거부</p>
  38. </div>
  39. </div>
  40. <div class="stats--card">
  41. <div class="stats--icon total">
  42. <v-icon>mdi-account-group</v-icon>
  43. </div>
  44. <div class="stats--content">
  45. <h3>{{ stats.total || 0 }}</h3>
  46. <p>총 요청 수</p>
  47. </div>
  48. </div>
  49. </div>
  50. <!-- 필터 및 검색 -->
  51. <div class="search--modules type2">
  52. <div class="search--inner">
  53. <div class="form--cont--filter">
  54. <v-select
  55. v-model="searchFilter.status"
  56. :items="statusOptions"
  57. variant="outlined"
  58. class="custom-select"
  59. label="상태"
  60. clearable
  61. >
  62. </v-select>
  63. </div>
  64. <div class="form--cont--filter">
  65. <v-select
  66. v-model="searchFilter.category"
  67. :items="categoryOptions"
  68. variant="outlined"
  69. class="custom-select"
  70. label="인플루언서 카테고리"
  71. clearable
  72. >
  73. </v-select>
  74. </div>
  75. <div class="form--cont--text">
  76. <v-text-field
  77. v-model="searchFilter.keyword"
  78. class="custom-input mini"
  79. style="width: 100%"
  80. placeholder="인플루언서명을 입력하세요"
  81. @keyup.enter="handleSearch"
  82. ></v-text-field>
  83. </div>
  84. </div>
  85. <v-btn
  86. class="custom-btn btn-blue mini sch--btn"
  87. @click="handleSearch"
  88. :loading="loading"
  89. >
  90. 검색
  91. </v-btn>
  92. </div>
  93. <!-- 인플루언서 승인 요청 목록 -->
  94. <div class="data--list--wrap">
  95. <div class="btn--actions--wrap">
  96. <div class="left--sections">
  97. <span class="result-count">
  98. 총 {{ pagination.totalCount || 0 }}개의 승인요청
  99. </span>
  100. </div>
  101. <div class="right--sections">
  102. <v-select
  103. v-model="sortOption"
  104. :items="sortOptions"
  105. variant="outlined"
  106. class="custom-select mini"
  107. @update:model-value="handleSort"
  108. >
  109. </v-select>
  110. </div>
  111. </div>
  112. <!-- 로딩 상태 -->
  113. <div v-if="loading" class="loading-wrap">
  114. <v-progress-circular indeterminate color="primary"></v-progress-circular>
  115. <p>승인요청을 불러오고 있습니다...</p>
  116. </div>
  117. <!-- 에러 상태 -->
  118. <div v-else-if="error" class="error-wrap">
  119. <v-alert type="error" dismissible @click:close="error = null">
  120. {{ error }}
  121. </v-alert>
  122. </div>
  123. <!-- 승인요청 리스트 -->
  124. <div v-else-if="requests && requests.length > 0" class="requests--list--wrap">
  125. <div class="requests--grid">
  126. <div
  127. v-for="request in requests"
  128. :key="request.SEQ"
  129. class="request--card"
  130. :class="getRequestStatusClass(request.CURRENT_STATUS)"
  131. >
  132. <!-- 카드 헤더 -->
  133. <div class="request--card--header">
  134. <div class="influencer--info">
  135. <div class="influencer--avatar">
  136. <v-img
  137. v-if="request.influencerAvatar"
  138. :src="request.influencerAvatar"
  139. :alt="request.influencerNickname + ' 프로필'"
  140. width="50"
  141. height="50"
  142. ></v-img>
  143. <div v-else class="no-avatar">
  144. {{ request.influencerNickname?.charAt(0) || "U" }}
  145. </div>
  146. </div>
  147. <div class="influencer--details">
  148. <div class="influencer--header">
  149. <h4>{{ request.influencerNickname || request.influencerName }}</h4>
  150. <p class="influencer--category">
  151. {{ getCategoryText(request.influencerCategory) }}
  152. </p>
  153. </div>
  154. <div class="influencer--contact">
  155. <p v-if="request.influencerEmail" class="contact--item">
  156. <v-icon size="small">mdi-email</v-icon>
  157. {{ request.influencerEmail }}
  158. </p>
  159. <p v-if="request.influencerPhone" class="contact--item">
  160. <v-icon size="small">mdi-phone</v-icon>
  161. {{ request.influencerPhone }}
  162. </p>
  163. <p v-if="request.influencerRegion" class="contact--item">
  164. <v-icon size="small">mdi-map-marker</v-icon>
  165. {{ request.influencerRegion }}
  166. </p>
  167. </div>
  168. <div class="influencer--meta">
  169. <span v-if="request.followerCount" class="meta--item">
  170. <v-icon size="small">mdi-account-group</v-icon>
  171. {{ formatNumber(request.followerCount) }} 팔로워
  172. </span>
  173. <span v-if="request.avgViews" class="meta--item">
  174. <v-icon size="small">mdi-eye</v-icon>
  175. 평균 {{ formatNumber(request.avgViews) }} 조회
  176. </span>
  177. <span v-if="request.engagementRate" class="meta--item">
  178. <v-icon size="small">mdi-chart-line</v-icon>
  179. 참여율 {{ request.engagementRate }}%
  180. </span>
  181. </div>
  182. <div
  183. v-if="request.influencerDescription"
  184. class="influencer--description"
  185. >
  186. <p>{{ request.influencerDescription }}</p>
  187. </div>
  188. <div v-if="request.influencerSnsChannels" class="influencer--sns">
  189. <div
  190. v-for="(channel, index) in parseSnsChannels(
  191. request.influencerSnsChannels
  192. )"
  193. :key="index"
  194. class="sns--item"
  195. >
  196. <v-icon size="small">{{ getSnsIcon(channel.platform) }}</v-icon>
  197. {{ channel.handle }}
  198. </div>
  199. </div>
  200. </div>
  201. </div>
  202. <div class="request--status">
  203. <div class="status--badges">
  204. <v-chip :color="getStatusColor(request.CURRENT_STATUS)" size="small">
  205. {{ getStatusText(request.CURRENT_STATUS) }}
  206. </v-chip>
  207. <v-chip
  208. v-if="request.ADD_INFO1 === 'REAPPLY'"
  209. color="orange"
  210. size="small"
  211. class="ml-2"
  212. >
  213. 재승인요청
  214. </v-chip>
  215. </div>
  216. <p class="request--date">{{ formatDate(request.REQUEST_DATE) }}</p>
  217. </div>
  218. </div>
  219. <!-- 카드 바디 -->
  220. <div class="request--card--body">
  221. <!-- 재승인 요청 안내 -->
  222. <div v-if="request.ADD_INFO1 === 'REAPPLY'" class="reapply--notice">
  223. <v-alert
  224. type="info"
  225. variant="tonal"
  226. density="compact"
  227. class="mb-4"
  228. >
  229. <v-icon size="16">mdi-refresh</v-icon>
  230. <span class="ml-2">
  231. 이전에 파트너십을 맺었던 인플루언서의 재승인 요청입니다.
  232. <br>이전 파트너십 종료일: {{ formatDate(request.ADD_INFO3) }}
  233. </span>
  234. </v-alert>
  235. </div>
  236. <div v-if="request.REQUEST_MESSAGE" class="request--message">
  237. <h5>{{ request.ADD_INFO1 === 'REAPPLY' ? '재승인 요청 메시지' : '요청 메시지' }}</h5>
  238. <p>"{{ request.REQUEST_MESSAGE }}"</p>
  239. </div>
  240. <div class="request--commission">
  241. <h5>수수료 조건</h5>
  242. <p>{{ request.COMMISSION_RATE || 0 }}%</p>
  243. </div>
  244. <div v-if="request.SPECIAL_CONDITIONS" class="request--conditions">
  245. <h5>특별 조건</h5>
  246. <p>"{{ request.SPECIAL_CONDITIONS }}"</p>
  247. </div>
  248. </div>
  249. <div class="request--card--footer">
  250. <div class="card--actions">
  251. <v-btn
  252. class="custom-btn mini btn-outline"
  253. @click="viewInfluencerDetail(request.INFLUENCER_SEQ)"
  254. >
  255. 프로필 보기
  256. </v-btn>
  257. <div v-if="request.CURRENT_STATUS === 'PENDING'" class="approval--actions">
  258. <v-btn
  259. class="custom-btn mini btn-red"
  260. @click="handleReject(request)"
  261. :loading="processing"
  262. >
  263. 거부
  264. </v-btn>
  265. <v-btn
  266. class="custom-btn mini btn-blue"
  267. @click="handleApprove(request)"
  268. :loading="processing"
  269. >
  270. {{ request.ADD_INFO1 === 'REAPPLY' ? '재승인' : '승인' }}
  271. </v-btn>
  272. </div>
  273. <div v-else-if="request.CURRENT_STATUS === 'APPROVED'" class="approved--actions">
  274. <v-btn
  275. class="custom-btn mini btn-outline"
  276. @click="viewRequestHistory(request.SEQ)"
  277. >
  278. 이력보기
  279. </v-btn>
  280. <v-btn
  281. class="custom-btn mini btn-terminate"
  282. @click="handleTerminate(request)"
  283. :loading="processing"
  284. >
  285. <v-icon left size="small">mdi-link-off</v-icon>
  286. 해지
  287. </v-btn>
  288. </div>
  289. <div
  290. v-else-if="request.CURRENT_STATUS === 'TERMINATED'"
  291. class="terminated--actions"
  292. >
  293. <v-btn
  294. class="custom-btn mini btn-outline"
  295. @click="viewRequestHistory(request.SEQ)"
  296. >
  297. 이력보기
  298. </v-btn>
  299. </div>
  300. <v-btn
  301. v-else
  302. class="custom-btn mini btn-outline"
  303. @click="viewRequestHistory(request.SEQ)"
  304. >
  305. 이력보기
  306. </v-btn>
  307. </div>
  308. </div>
  309. </div>
  310. </div>
  311. <!-- 페이지네이션 -->
  312. <div class="pagination-wrap" v-if="pagination.totalPages > 1">
  313. <v-pagination
  314. v-model="currentPage"
  315. :length="pagination.totalPages"
  316. :total-visible="7"
  317. @update:model-value="handlePageChange"
  318. ></v-pagination>
  319. </div>
  320. </div>
  321. <!-- 검색 결과 없음 -->
  322. <div v-else class="no-data-wrap">
  323. <div class="no-data">
  324. <v-icon size="64" color="grey-lighten-1">mdi-account-search</v-icon>
  325. <h3>승인요청이 없습니다</h3>
  326. <p>아직 인플루언서로부터 승인요청이 없습니다</p>
  327. </div>
  328. </div>
  329. </div>
  330. <!-- 승인 확인 모달 -->
  331. <v-dialog v-model="approveModal.show" max-width="500px">
  332. <v-card>
  333. <v-card-title class="text-h5 text-success">
  334. <v-icon left>mdi-check-circle</v-icon>
  335. 승인 확인
  336. </v-card-title>
  337. <v-card-text>
  338. <div class="approve--content">
  339. <div class="influencer--summary">
  340. <div class="influencer--avatar--small">
  341. <v-img
  342. v-if="approveModal.request?.influencerAvatar"
  343. :src="approveModal.request.influencerAvatar"
  344. width="40"
  345. height="40"
  346. ></v-img>
  347. <div v-else class="no-avatar--small">
  348. {{ approveModal.request?.influencerNickname?.charAt(0) || "U" }}
  349. </div>
  350. </div>
  351. <div>
  352. <h4>{{ approveModal.request?.influencerNickname }}</h4>
  353. <p>{{ getCategoryText(approveModal.request?.influencerCategory) }}</p>
  354. </div>
  355. </div>
  356. <p>이 인플루언서의 승인요청을 승인하시겠습니까?</p>
  357. <v-textarea
  358. v-model="approveModal.approveMessage"
  359. label="승인 메시지 (선택사항)"
  360. placeholder="인플루언서에게 전달할 메시지를 입력해주세요..."
  361. rows="3"
  362. counter="300"
  363. maxlength="300"
  364. class="mt-4"
  365. ></v-textarea>
  366. </div>
  367. </v-card-text>
  368. <v-card-actions>
  369. <v-spacer></v-spacer>
  370. <v-btn color="grey" variant="text" @click="closeApproveModal">취소</v-btn>
  371. <v-btn color="success" @click="confirmApprove" :loading="processing">
  372. 승인하기
  373. </v-btn>
  374. </v-card-actions>
  375. </v-card>
  376. </v-dialog>
  377. <!-- 거부 확인 모달 -->
  378. <v-dialog v-model="rejectModal.show" max-width="500px">
  379. <v-card>
  380. <v-card-title class="text-h5 text-error">
  381. <v-icon left>mdi-close-circle</v-icon>
  382. 거부 확인
  383. </v-card-title>
  384. <v-card-text>
  385. <div class="reject--content">
  386. <div class="influencer--summary">
  387. <div class="influencer--avatar--small">
  388. <v-img
  389. v-if="rejectModal.request?.influencerAvatar"
  390. :src="rejectModal.request.influencerAvatar"
  391. width="40"
  392. height="40"
  393. ></v-img>
  394. <div v-else class="no-avatar--small">
  395. {{ rejectModal.request?.influencerNickname?.charAt(0) || "U" }}
  396. </div>
  397. </div>
  398. <div>
  399. <h4>{{ rejectModal.request?.influencerNickname }}</h4>
  400. <p>{{ getCategoryText(rejectModal.request?.influencerCategory) }}</p>
  401. </div>
  402. </div>
  403. <p>이 인플루언서의 승인요청을 거부하시겠습니까?</p>
  404. <v-textarea
  405. v-model="rejectModal.rejectReason"
  406. label="거부 사유"
  407. placeholder="거부 사유를 입력해주세요..."
  408. rows="4"
  409. counter="500"
  410. maxlength="500"
  411. class="mt-4"
  412. required
  413. ></v-textarea>
  414. </div>
  415. </v-card-text>
  416. <v-card-actions>
  417. <v-spacer></v-spacer>
  418. <v-btn color="grey" variant="text" @click="closeRejectModal">취소</v-btn>
  419. <v-btn color="error" @click="confirmReject" :loading="processing">
  420. 거부하기
  421. </v-btn>
  422. </v-card-actions>
  423. </v-card>
  424. </v-dialog>
  425. <!-- 해지 확인 모달 -->
  426. <v-dialog v-model="terminateModal.show" max-width="500px">
  427. <v-card>
  428. <v-card-title class="text-h5 text-warning">
  429. <v-icon left>mdi-link-off</v-icon>
  430. 파트너십 해지 확인
  431. </v-card-title>
  432. <v-card-text>
  433. <div class="terminate--content">
  434. <div class="influencer--summary">
  435. <div class="influencer--avatar--small">
  436. <v-img
  437. v-if="terminateModal.request?.influencerAvatar"
  438. :src="terminateModal.request.influencerAvatar"
  439. width="40"
  440. height="40"
  441. ></v-img>
  442. <div v-else class="no-avatar--small">
  443. {{ terminateModal.request?.influencerNickname?.charAt(0) || "U" }}
  444. </div>
  445. </div>
  446. <div>
  447. <h4>{{ terminateModal.request?.influencerNickname }}</h4>
  448. <p>{{ getCategoryText(terminateModal.request?.influencerCategory) }}</p>
  449. </div>
  450. </div>
  451. <v-alert type="warning" class="mb-4">
  452. <strong>주의:</strong> 파트너십을 해지하면 협업 관계가 종료되며, 이 작업은
  453. 되돌릴 수 없습니다.
  454. </v-alert>
  455. <p>이 인플루언서와의 파트너십을 해지하시겠습니까?</p>
  456. <v-textarea
  457. v-model="terminateModal.terminateReason"
  458. label="해지 사유"
  459. placeholder="해지 사유를 입력해주세요..."
  460. rows="4"
  461. counter="500"
  462. maxlength="500"
  463. class="mt-4"
  464. required
  465. ></v-textarea>
  466. </div>
  467. </v-card-text>
  468. <v-card-actions>
  469. <v-spacer></v-spacer>
  470. <v-btn color="grey" variant="text" @click="closeTerminateModal">취소</v-btn>
  471. <v-btn
  472. class="btn-terminate-confirm"
  473. @click="confirmTerminate"
  474. :loading="processing"
  475. >
  476. <v-icon left>mdi-link-off</v-icon>
  477. 해지하기
  478. </v-btn>
  479. </v-card-actions>
  480. </v-card>
  481. </v-dialog>
  482. </div>
  483. </template>
  484. <script setup>
  485. import { ref, onMounted, computed } from "vue";
  486. import { useRouter } from "vue-router";
  487. /************************************************************************
  488. | 레이아웃
  489. ************************************************************************/
  490. definePageMeta({
  491. layout: "default",
  492. });
  493. /************************************************************************
  494. | 스토어 & 라우터
  495. ************************************************************************/
  496. const router = useRouter();
  497. const { $toast } = useNuxtApp();
  498. /************************************************************************
  499. | 반응형 데이터
  500. ************************************************************************/
  501. const pageId = ref("인플루언서 승인요청 관리");
  502. const loading = ref(false);
  503. const processing = ref(false);
  504. const error = ref(null);
  505. const currentPage = ref(1);
  506. // 검색 필터
  507. const searchFilter = ref({
  508. keyword: "",
  509. status: "",
  510. category: "",
  511. });
  512. // 정렬 옵션
  513. const sortOption = ref("latest");
  514. const sortOptions = ref([
  515. { title: "최신순", value: "latest" },
  516. { title: "오래된순", value: "oldest" },
  517. { title: "마감임박순", value: "expiring" },
  518. ]);
  519. // 상태 옵션
  520. const statusOptions = ref([
  521. { title: "전체", value: "" },
  522. { title: "대기중", value: "PENDING" },
  523. { title: "승인완료", value: "APPROVED" },
  524. { title: "거부됨", value: "REJECTED" },
  525. { title: "해지됨", value: "TERMINATED" },
  526. ]);
  527. // 카테고리 옵션
  528. const categoryOptions = ref([
  529. { title: "전체", value: "" },
  530. { title: "패션·뷰티", value: "FASHION_BEAUTY" },
  531. { title: "식품·건강", value: "FOOD_HEALTH" },
  532. { title: "라이프스타일", value: "LIFESTYLE" },
  533. { title: "테크·가전", value: "TECH_ELECTRONICS" },
  534. { title: "스포츠·레저", value: "SPORTS_LEISURE" },
  535. { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" },
  536. ]);
  537. // 데이터
  538. const requests = ref([]);
  539. const stats = ref({
  540. pending: 0,
  541. approved: 0,
  542. rejected: 0,
  543. total: 0,
  544. });
  545. const pagination = ref({
  546. currentPage: 1,
  547. totalPages: 1,
  548. totalCount: 0,
  549. pageSize: 12,
  550. });
  551. // 승인 모달
  552. const approveModal = ref({
  553. show: false,
  554. request: null,
  555. approveMessage: "",
  556. });
  557. // 거부 모달
  558. const rejectModal = ref({
  559. show: false,
  560. request: null,
  561. rejectReason: "",
  562. });
  563. // 해지 모달
  564. const terminateModal = ref({
  565. show: false,
  566. request: null,
  567. terminateReason: "",
  568. });
  569. /************************************************************************
  570. | computed
  571. ************************************************************************/
  572. const currentUser = computed(() => {
  573. try {
  574. const authStore = localStorage.getItem("authStore");
  575. if (!authStore) {
  576. console.warn("⚠️ authStore가 localStorage에 없습니다");
  577. return {};
  578. }
  579. const parsedStore = JSON.parse(authStore);
  580. const authData = parsedStore?.auth || {};
  581. console.log("🔍 localStorage authStore:", parsedStore);
  582. console.log("🔍 currentUser (벤더 대시보드):", authData);
  583. // seq 필드가 없으면 다른 가능한 필드들 시도
  584. if (!authData.seq) {
  585. authData.seq = authData.SEQ || authData.id || authData.user_seq || authData.userSeq;
  586. console.log("🔧 seq 필드 보정:", authData.seq);
  587. }
  588. return authData;
  589. } catch (error) {
  590. console.error("❌ authStore 파싱 오류:", error);
  591. return {};
  592. }
  593. });
  594. /************************************************************************
  595. | 메서드
  596. ************************************************************************/
  597. const handleSearch = async () => {
  598. currentPage.value = 1;
  599. await loadRequests();
  600. };
  601. const handlePageChange = async (page) => {
  602. currentPage.value = page;
  603. await loadRequests();
  604. };
  605. const handleSort = async () => {
  606. currentPage.value = 1;
  607. await loadRequests();
  608. };
  609. const loadRequests = async () => {
  610. try {
  611. loading.value = true;
  612. error.value = null;
  613. const params = {
  614. vendorSeq: currentUser.value.seq,
  615. keyword: searchFilter.value.keyword,
  616. status: searchFilter.value.status,
  617. category: searchFilter.value.category,
  618. sortBy: sortOption.value,
  619. page: currentPage.value,
  620. size: pagination.value.pageSize,
  621. };
  622. console.log("🔍 loadRequests 호출됨:", params);
  623. useAxios()
  624. .post("/api/vendor-influencer/requests", params)
  625. .then((res) => {
  626. console.log("📥 API 응답:", res.data);
  627. if (res.data.success) {
  628. const items = res.data.data.items || []; // 빈 배열로 기본값 설정
  629. console.log("📋 받아온 요청 목록:", items.length, items);
  630. // SEQ 중복 확인
  631. if (items.length > 0) {
  632. const seqs = items.map((item) => item.SEQ);
  633. const uniqueSeqs = [...new Set(seqs)];
  634. if (seqs.length !== uniqueSeqs.length) {
  635. console.warn("⚠️ 중복된 SEQ 발견:", seqs);
  636. }
  637. }
  638. requests.value = items;
  639. pagination.value = {
  640. totalCount: res.data.data.total || 0,
  641. currentPage: res.data.data.page || 1,
  642. totalPages: res.data.data.totalPages || 1,
  643. pageSize: res.data.data.size || 20
  644. };
  645. stats.value = res.data.data.stats || {
  646. pending: 0,
  647. approved: 0,
  648. rejected: 0,
  649. total: 0
  650. };
  651. } else {
  652. error.value =
  653. res.data.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
  654. }
  655. })
  656. .catch((err) => {
  657. error.value = err.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
  658. })
  659. .finally(() => {
  660. loading.value = false;
  661. });
  662. } catch (err) {
  663. error.value = err.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
  664. loading.value = false;
  665. }
  666. };
  667. const handleApprove = (request) => {
  668. approveModal.value = {
  669. show: true,
  670. request: request,
  671. approveMessage: "",
  672. };
  673. };
  674. const closeApproveModal = () => {
  675. approveModal.value = {
  676. show: false,
  677. request: null,
  678. approveMessage: "",
  679. };
  680. };
  681. const confirmApprove = async () => {
  682. try {
  683. processing.value = true;
  684. const params = {
  685. mappingSeq: approveModal.value.request.SEQ,
  686. action: "APPROVE",
  687. processedBy: currentUser.value.seq,
  688. responseMessage: approveModal.value.approveMessage,
  689. };
  690. console.log("🔍 현재 사용자 정보:", currentUser.value);
  691. console.log("🔍 processedBy 값:", currentUser.value.seq);
  692. console.log("✅ 승인 처리 시작:", params);
  693. useAxios()
  694. .post("/api/vendor-influencer/approve", params)
  695. .then((res) => {
  696. console.log("📥 승인 처리 응답:", res.data);
  697. if (res.data.success) {
  698. $toast.success("승인요청이 승인되었습니다.");
  699. closeApproveModal();
  700. console.log("🔄 승인 후 목록 새로고침");
  701. loadRequests();
  702. } else {
  703. console.error("❌ 승인 처리 실패:", res.data);
  704. $toast.error(res.data.message || "승인 처리 중 오류가 발생했습니다.");
  705. }
  706. })
  707. .catch((err) => {
  708. $toast.error(err.message || "승인 처리 중 오류가 발생했습니다.");
  709. })
  710. .finally(() => {
  711. processing.value = false;
  712. });
  713. } catch (err) {
  714. $toast.error(err.message || "승인 처리 중 오류가 발생했습니다.");
  715. processing.value = false;
  716. }
  717. };
  718. const handleReject = (request) => {
  719. rejectModal.value = {
  720. show: true,
  721. request: request,
  722. rejectReason: "",
  723. };
  724. };
  725. const closeRejectModal = () => {
  726. rejectModal.value = {
  727. show: false,
  728. request: null,
  729. rejectReason: "",
  730. };
  731. };
  732. const confirmReject = async () => {
  733. if (!rejectModal.value.rejectReason.trim()) {
  734. $toast.error("거부 사유를 입력해주세요.");
  735. return;
  736. }
  737. try {
  738. processing.value = true;
  739. const params = {
  740. mappingSeq: rejectModal.value.request.SEQ,
  741. action: "REJECT",
  742. processedBy: currentUser.value.seq,
  743. responseMessage: rejectModal.value.rejectReason,
  744. };
  745. useAxios()
  746. .post("/api/vendor-influencer/approve", params)
  747. .then((res) => {
  748. if (res.data.success) {
  749. $toast.success("승인요청이 거부되었습니다.");
  750. closeRejectModal();
  751. loadRequests();
  752. } else {
  753. $toast.error(res.data.message || "거부 처리 중 오류가 발생했습니다.");
  754. }
  755. })
  756. .catch((err) => {
  757. $toast.error(err.message || "거부 처리 중 오류가 발생했습니다.");
  758. })
  759. .finally(() => {
  760. processing.value = false;
  761. });
  762. } catch (err) {
  763. $toast.error(err.message || "거부 처리 중 오류가 발생했습니다.");
  764. processing.value = false;
  765. }
  766. };
  767. const viewInfluencerDetail = (influencerSeq) => {
  768. router.push(`/view/influencer/${influencerSeq}`);
  769. };
  770. const viewRequestHistory = (requestSeq) => {
  771. router.push(`/view/vendor/request-history/${requestSeq}`);
  772. };
  773. /**
  774. * 파트너십 해지
  775. */
  776. const handleTerminate = (request) => {
  777. terminateModal.value = {
  778. show: true,
  779. request: request,
  780. terminateReason: "",
  781. };
  782. };
  783. const closeTerminateModal = () => {
  784. terminateModal.value = {
  785. show: false,
  786. request: null,
  787. terminateReason: "",
  788. };
  789. };
  790. const confirmTerminate = async () => {
  791. if (!terminateModal.value.terminateReason.trim()) {
  792. $toast.error("해지 사유를 입력해주세요.");
  793. return;
  794. }
  795. try {
  796. processing.value = true;
  797. const params = {
  798. mappingSeq: terminateModal.value.request.SEQ,
  799. terminateReason: terminateModal.value.terminateReason,
  800. terminatedBy: currentUser.value.seq,
  801. };
  802. console.log("🔗 파트너십 해지 처리 시작:", params);
  803. useAxios()
  804. .post("/api/vendor-influencer/terminate", params)
  805. .then((res) => {
  806. console.log("📥 해지 처리 응답:", res.data);
  807. if (res.data.success) {
  808. $toast.success("파트너십이 해지되었습니다.");
  809. closeTerminateModal();
  810. console.log("🔄 해지 후 목록 새로고침");
  811. loadRequests();
  812. } else {
  813. console.error("❌ 해지 처리 실패:", res.data);
  814. $toast.error(res.data.message || "해지 처리 중 오류가 발생했습니다.");
  815. }
  816. })
  817. .catch((err) => {
  818. $toast.error(err.message || "해지 처리 중 오류가 발생했습니다.");
  819. })
  820. .finally(() => {
  821. processing.value = false;
  822. });
  823. } catch (err) {
  824. $toast.error(err.message || "해지 처리 중 오류가 발생했습니다.");
  825. processing.value = false;
  826. }
  827. };
  828. // 유틸리티 함수들
  829. const getCategoryText = (category) => {
  830. const categoryMap = {
  831. FASHION_BEAUTY: "패션·뷰티",
  832. FOOD_HEALTH: "식품·건강",
  833. LIFESTYLE: "라이프스타일",
  834. TECH_ELECTRONICS: "테크·가전",
  835. SPORTS_LEISURE: "스포츠·레저",
  836. CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
  837. };
  838. return categoryMap[category] || category || "기타";
  839. };
  840. const getStatusText = (status) => {
  841. const statusMap = {
  842. PENDING: "대기중",
  843. APPROVED: "승인완료",
  844. REJECTED: "거절됨",
  845. TERMINATED: "해지됨",
  846. EXPIRED: "만료됨",
  847. };
  848. return statusMap[status] || status || "알 수 없음";
  849. };
  850. const getStatusColor = (status) => {
  851. const colorMap = {
  852. PENDING: "orange",
  853. APPROVED: "success",
  854. REJECTED: "error",
  855. TERMINATED: "warning",
  856. EXPIRED: "grey",
  857. };
  858. return colorMap[status] || "grey";
  859. };
  860. const getRequestStatusClass = (status) => {
  861. return `request-status-${status?.toLowerCase() || "unknown"}`;
  862. };
  863. const formatDate = (dateString) => {
  864. return new Date(dateString).toLocaleDateString("ko-KR");
  865. };
  866. const formatNumber = (num) => {
  867. if (!num) return "0";
  868. if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
  869. if (num >= 1000) return (num / 1000).toFixed(1) + "K";
  870. return num.toString();
  871. };
  872. const parseSnsChannels = (snsChannels) => {
  873. try {
  874. return JSON.parse(snsChannels);
  875. } catch (e) {
  876. return [];
  877. }
  878. };
  879. const getSnsIcon = (platform) => {
  880. const iconMap = {
  881. instagram: "mdi-instagram",
  882. youtube: "mdi-youtube",
  883. tiktok: "mdi-music-note",
  884. blog: "mdi-post",
  885. facebook: "mdi-facebook",
  886. twitter: "mdi-twitter",
  887. };
  888. return iconMap[platform.toLowerCase()] || "mdi-link";
  889. };
  890. /************************************************************************
  891. | 라이프사이클
  892. ************************************************************************/
  893. onMounted(async () => {
  894. console.log("🚀 influencer-requests 컴포넌트 마운트됨");
  895. await loadRequests();
  896. });
  897. </script>
  898. <style scoped>
  899. .stats--cards--wrap {
  900. display: grid;
  901. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  902. gap: 20px;
  903. margin-bottom: 30px;
  904. }
  905. .stats--card {
  906. background: white;
  907. border-radius: 12px;
  908. padding: 20px;
  909. display: flex;
  910. align-items: center;
  911. gap: 16px;
  912. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  913. }
  914. .stats--icon {
  915. width: 50px;
  916. height: 50px;
  917. border-radius: 10px;
  918. display: flex;
  919. align-items: center;
  920. justify-content: center;
  921. color: white;
  922. }
  923. .stats--icon.pending {
  924. background: #ff9800;
  925. }
  926. .stats--icon.approved {
  927. background: #4caf50;
  928. }
  929. .stats--icon.rejected {
  930. background: #f44336;
  931. }
  932. .stats--icon.total {
  933. background: #2196f3;
  934. }
  935. .stats--content h3 {
  936. margin: 0;
  937. font-size: 24px;
  938. font-weight: 700;
  939. color: #333;
  940. }
  941. .stats--content p {
  942. margin: 0;
  943. font-size: 14px;
  944. color: #666;
  945. }
  946. .requests--list--wrap {
  947. margin-top: 20px;
  948. }
  949. .requests--grid {
  950. display: grid;
  951. grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
  952. gap: 20px;
  953. margin-bottom: 20px;
  954. }
  955. .request--card {
  956. background: white;
  957. border-radius: 12px;
  958. padding: 20px;
  959. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  960. transition: transform 0.2s, box-shadow 0.2s;
  961. border-left: 4px solid #e0e0e0;
  962. }
  963. .request--card:hover {
  964. transform: translateY(-2px);
  965. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  966. }
  967. .request--card.request-status-pending {
  968. border-left-color: #ff9800;
  969. }
  970. .request--card.request-status-approved {
  971. border-left-color: #4caf50;
  972. }
  973. .request--card.request-status-rejected {
  974. border-left-color: #f44336;
  975. }
  976. .request--card.request-status-terminated {
  977. border-left-color: #ff9800;
  978. }
  979. .request--card--header {
  980. display: flex;
  981. justify-content: space-between;
  982. align-items: flex-start;
  983. margin-bottom: 16px;
  984. }
  985. .influencer--info {
  986. display: flex;
  987. gap: 12px;
  988. flex: 1;
  989. }
  990. .influencer--avatar {
  991. width: 50px;
  992. height: 50px;
  993. border-radius: 50%;
  994. overflow: hidden;
  995. flex-shrink: 0;
  996. display: flex;
  997. align-items: center;
  998. justify-content: center;
  999. background: #f5f5f5;
  1000. }
  1001. .no-avatar {
  1002. font-size: 20px;
  1003. font-weight: bold;
  1004. color: #666;
  1005. }
  1006. .influencer--details h4 {
  1007. margin: 0 0 4px 0;
  1008. font-size: 16px;
  1009. font-weight: 600;
  1010. }
  1011. .influencer--category {
  1012. color: #666;
  1013. font-size: 14px;
  1014. margin: 0 0 8px 0;
  1015. }
  1016. .influencer--meta {
  1017. display: flex;
  1018. flex-direction: column;
  1019. gap: 2px;
  1020. }
  1021. .influencer--meta span {
  1022. font-size: 12px;
  1023. color: #888;
  1024. }
  1025. .request--status {
  1026. text-align: right;
  1027. flex-shrink: 0;
  1028. }
  1029. .request--date {
  1030. margin: 8px 0 0;
  1031. font-size: 12px;
  1032. color: #999;
  1033. }
  1034. .request--card--body {
  1035. margin-bottom: 16px;
  1036. }
  1037. .request--message,
  1038. .request--conditions {
  1039. margin-bottom: 12px;
  1040. }
  1041. .request--message h5,
  1042. .request--conditions h5 {
  1043. margin: 0 0 8px 0;
  1044. font-size: 14px;
  1045. font-weight: 600;
  1046. color: #333;
  1047. }
  1048. .request--message p {
  1049. margin: 0;
  1050. font-size: 14px;
  1051. color: #666;
  1052. font-style: italic;
  1053. }
  1054. .condition--item {
  1055. display: flex;
  1056. gap: 8px;
  1057. margin-bottom: 4px;
  1058. }
  1059. .condition--label {
  1060. font-size: 13px;
  1061. color: #666;
  1062. min-width: 80px;
  1063. }
  1064. .condition--value {
  1065. font-size: 13px;
  1066. color: #333;
  1067. font-weight: 500;
  1068. }
  1069. .expire--info {
  1070. display: flex;
  1071. align-items: center;
  1072. gap: 6px;
  1073. font-size: 12px;
  1074. color: #ff9800;
  1075. background: #fff8e1;
  1076. padding: 6px 10px;
  1077. border-radius: 6px;
  1078. }
  1079. .request--card--footer {
  1080. border-top: 1px solid #f0f0f0;
  1081. padding-top: 16px;
  1082. }
  1083. .card--actions {
  1084. display: flex;
  1085. justify-content: space-between;
  1086. align-items: center;
  1087. }
  1088. .approval--actions,
  1089. .approved--actions,
  1090. .terminated--actions {
  1091. display: flex;
  1092. gap: 8px;
  1093. }
  1094. .loading-wrap,
  1095. .error-wrap,
  1096. .no-data-wrap {
  1097. display: flex;
  1098. flex-direction: column;
  1099. align-items: center;
  1100. justify-content: center;
  1101. padding: 60px 20px;
  1102. }
  1103. .no-data {
  1104. text-align: center;
  1105. }
  1106. .no-data h3 {
  1107. margin: 16px 0 8px;
  1108. color: #666;
  1109. }
  1110. .no-data p {
  1111. color: #999;
  1112. }
  1113. .pagination-wrap {
  1114. display: flex;
  1115. justify-content: center;
  1116. margin-top: 20px;
  1117. }
  1118. .approve--content,
  1119. .reject--content,
  1120. .terminate--content {
  1121. padding: 8px 0;
  1122. }
  1123. .influencer--summary {
  1124. display: flex;
  1125. align-items: center;
  1126. gap: 12px;
  1127. padding: 12px;
  1128. background: #f8f9fa;
  1129. border-radius: 8px;
  1130. margin-bottom: 16px;
  1131. }
  1132. .influencer--avatar--small {
  1133. width: 40px;
  1134. height: 40px;
  1135. border-radius: 50%;
  1136. overflow: hidden;
  1137. flex-shrink: 0;
  1138. display: flex;
  1139. align-items: center;
  1140. justify-content: center;
  1141. background: #f5f5f5;
  1142. }
  1143. .no-avatar--small {
  1144. font-size: 16px;
  1145. font-weight: bold;
  1146. color: #666;
  1147. }
  1148. .result-count {
  1149. font-size: 14px;
  1150. color: #666;
  1151. font-weight: 500;
  1152. }
  1153. .influencer--contact {
  1154. margin: 4px 0;
  1155. }
  1156. .contact--item {
  1157. display: flex;
  1158. align-items: center;
  1159. gap: 6px;
  1160. font-size: 13px;
  1161. color: #666;
  1162. margin: 2px 0;
  1163. }
  1164. .contact--item .v-icon {
  1165. color: #999;
  1166. }
  1167. .influencer--header {
  1168. margin-bottom: 8px;
  1169. }
  1170. .influencer--contact {
  1171. margin: 8px 0;
  1172. padding: 8px;
  1173. background: #f8f9fa;
  1174. border-radius: 6px;
  1175. }
  1176. .contact--item {
  1177. display: flex;
  1178. align-items: center;
  1179. gap: 6px;
  1180. font-size: 13px;
  1181. color: #666;
  1182. margin: 4px 0;
  1183. }
  1184. .influencer--meta {
  1185. display: flex;
  1186. flex-wrap: wrap;
  1187. gap: 12px;
  1188. margin: 8px 0;
  1189. }
  1190. .meta--item {
  1191. display: flex;
  1192. align-items: center;
  1193. gap: 4px;
  1194. font-size: 13px;
  1195. color: #555;
  1196. background: #f0f0f0;
  1197. padding: 4px 8px;
  1198. border-radius: 4px;
  1199. }
  1200. .influencer--description {
  1201. margin: 8px 0;
  1202. font-size: 13px;
  1203. color: #666;
  1204. line-height: 1.4;
  1205. }
  1206. .influencer--sns {
  1207. display: flex;
  1208. flex-wrap: wrap;
  1209. gap: 8px;
  1210. margin-top: 8px;
  1211. }
  1212. .sns--item {
  1213. display: flex;
  1214. align-items: center;
  1215. gap: 4px;
  1216. font-size: 12px;
  1217. color: #555;
  1218. background: #eef2ff;
  1219. padding: 4px 8px;
  1220. border-radius: 4px;
  1221. }
  1222. /* 해지 버튼 전용 스타일 */
  1223. .btn-terminate {
  1224. background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
  1225. color: white;
  1226. border: none;
  1227. font-weight: 700;
  1228. min-width: 100px;
  1229. padding: 8px 16px;
  1230. box-shadow: 0 4px 12px rgba(229, 62, 62, 0.3);
  1231. transition: all 0.3s ease;
  1232. }
  1233. .btn-terminate:hover {
  1234. transform: translateY(-2px);
  1235. box-shadow: 0 6px 20px rgba(229, 62, 62, 0.4);
  1236. background: linear-gradient(135deg, #c53030 0%, #9b2c2c 100%);
  1237. }
  1238. .btn-terminate:active {
  1239. transform: translateY(0);
  1240. }
  1241. .btn-terminate:disabled {
  1242. background: #e2e8f0;
  1243. color: #a0aec0;
  1244. box-shadow: none;
  1245. transform: none !important;
  1246. }
  1247. .btn-green {
  1248. background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
  1249. color: white;
  1250. border: none;
  1251. font-weight: 600;
  1252. min-width: 140px;
  1253. padding: 8px 16px;
  1254. box-shadow: 0 4px 12px rgba(56, 161, 105, 0.3);
  1255. transition: all 0.3s ease;
  1256. }
  1257. .btn-green:hover {
  1258. transform: translateY(-2px);
  1259. box-shadow: 0 6px 20px rgba(56, 161, 105, 0.4);
  1260. background: linear-gradient(135deg, #2f855a 0%, #276749 100%);
  1261. }
  1262. .btn-green:active {
  1263. transform: translateY(0);
  1264. }
  1265. .btn-green:disabled {
  1266. background: #e6fffa;
  1267. color: #38a169;
  1268. border: 1px solid #38a169;
  1269. box-shadow: none;
  1270. transform: none !important;
  1271. opacity: 0.7;
  1272. }
  1273. /* 해지 확인 모달 버튼 스타일 */
  1274. .btn-terminate-confirm {
  1275. background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
  1276. color: white !important;
  1277. font-weight: 700 !important;
  1278. font-size: 14px !important;
  1279. padding: 12px 24px !important;
  1280. border-radius: 8px !important;
  1281. border: none !important;
  1282. box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4) !important;
  1283. transition: all 0.3s ease !important;
  1284. text-transform: none !important;
  1285. letter-spacing: 0.5px !important;
  1286. min-width: 120px !important;
  1287. }
  1288. .btn-terminate-confirm:hover {
  1289. background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%) !important;
  1290. box-shadow: 0 6px 16px rgba(220, 38, 38, 0.5) !important;
  1291. transform: translateY(-2px) !important;
  1292. }
  1293. .btn-terminate-confirm:active {
  1294. transform: translateY(0) !important;
  1295. box-shadow: 0 3px 8px rgba(220, 38, 38, 0.4) !important;
  1296. }
  1297. .btn-terminate-confirm .v-icon {
  1298. color: white !important;
  1299. margin-right: 6px !important;
  1300. }
  1301. .btn-terminate-confirm:disabled {
  1302. background: #fca5a5 !important;
  1303. color: #9ca3af !important;
  1304. box-shadow: none !important;
  1305. transform: none !important;
  1306. }
  1307. .status--badges {
  1308. display: flex;
  1309. align-items: center;
  1310. gap: 8px;
  1311. flex-wrap: wrap;
  1312. }
  1313. .status--badges .v-chip {
  1314. font-weight: 500;
  1315. }
  1316. </style>