| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416 |
- <template>
- <div>
- <div class="inner--headers">
- <h2>{{ pageId }}</h2>
- <div class="bread--crumbs--wrap">
- <span>홈</span>
- <span>벤더 대시보드</span>
- <span>{{ pageId }}</span>
- </div>
- </div>
- <!-- 통계 카드 -->
- <div class="stats--cards--wrap">
- <div class="stats--card">
- <div class="stats--icon pending">
- <v-icon>mdi-clock-outline</v-icon>
- </div>
- <div class="stats--content">
- <h3>{{ stats.pending || 0 }}</h3>
- <p>대기 중인 승인요청</p>
- </div>
- </div>
- <div class="stats--card">
- <div class="stats--icon approved">
- <v-icon>mdi-check-circle</v-icon>
- </div>
- <div class="stats--content">
- <h3>{{ stats.approved || 0 }}</h3>
- <p>승인 완료</p>
- </div>
- </div>
- <div class="stats--card">
- <div class="stats--icon rejected">
- <v-icon>mdi-close-circle</v-icon>
- </div>
- <div class="stats--content">
- <h3>{{ stats.rejected || 0 }}</h3>
- <p>거부</p>
- </div>
- </div>
- <div class="stats--card">
- <div class="stats--icon total">
- <v-icon>mdi-account-group</v-icon>
- </div>
- <div class="stats--content">
- <h3>{{ stats.total || 0 }}</h3>
- <p>총 요청 수</p>
- </div>
- </div>
- </div>
- <!-- 필터 및 검색 -->
- <div class="search--modules type2">
- <div class="search--inner">
- <div class="form--cont--filter">
- <v-select
- v-model="searchFilter.status"
- :items="statusOptions"
- variant="outlined"
- class="custom-select"
- label="상태"
- clearable
- >
- </v-select>
- </div>
- <div class="form--cont--filter">
- <v-select
- v-model="searchFilter.category"
- :items="categoryOptions"
- variant="outlined"
- class="custom-select"
- label="인플루언서 카테고리"
- clearable
- >
- </v-select>
- </div>
- <div class="form--cont--text">
- <v-text-field
- v-model="searchFilter.keyword"
- class="custom-input mini"
- style="width: 100%"
- placeholder="인플루언서명을 입력하세요"
- @keyup.enter="handleSearch"
- ></v-text-field>
- </div>
- </div>
- <v-btn
- class="custom-btn btn-blue mini sch--btn"
- @click="handleSearch"
- :loading="loading"
- >
- 검색
- </v-btn>
- </div>
- <!-- 인플루언서 승인 요청 목록 -->
- <div class="data--list--wrap">
- <div class="btn--actions--wrap">
- <div class="left--sections">
- <span class="result-count">
- 총 {{ pagination.totalCount || 0 }}개의 승인요청
- </span>
- </div>
- <div class="right--sections">
- <v-select
- v-model="sortOption"
- :items="sortOptions"
- variant="outlined"
- class="custom-select mini"
- @update:model-value="handleSort"
- >
- </v-select>
- </div>
- </div>
- <!-- 로딩 상태 -->
- <div v-if="loading" class="loading-wrap">
- <v-progress-circular indeterminate color="primary"></v-progress-circular>
- <p>승인요청을 불러오고 있습니다...</p>
- </div>
- <!-- 에러 상태 -->
- <div v-else-if="error" class="error-wrap">
- <v-alert type="error" dismissible @click:close="error = null">
- {{ error }}
- </v-alert>
- </div>
- <!-- 승인요청 리스트 -->
- <div v-else-if="requests.length > 0" class="requests--list--wrap">
- <div class="requests--grid">
- <div
- v-for="request in requests"
- :key="request.SEQ"
- class="request--card"
- :class="getRequestStatusClass(request.STATUS)"
- >
- <!-- 카드 헤더 -->
- <div class="request--card--header">
- <div class="influencer--info">
- <div class="influencer--avatar">
- <v-img
- v-if="request.influencerAvatar"
- :src="request.influencerAvatar"
- :alt="request.influencerNickname + ' 프로필'"
- width="50"
- height="50"
- ></v-img>
- <div v-else class="no-avatar">
- {{ request.influencerNickname?.charAt(0) || "U" }}
- </div>
- </div>
- <div class="influencer--details">
- <div class="influencer--header">
- <h4>{{ request.influencerNickname || request.influencerName }}</h4>
- <p class="influencer--category">
- {{ getCategoryText(request.influencerCategory) }}
- </p>
- </div>
- <div class="influencer--contact">
- <p v-if="request.influencerEmail" class="contact--item">
- <v-icon size="small">mdi-email</v-icon>
- {{ request.influencerEmail }}
- </p>
- <p v-if="request.influencerPhone" class="contact--item">
- <v-icon size="small">mdi-phone</v-icon>
- {{ request.influencerPhone }}
- </p>
- <p v-if="request.influencerRegion" class="contact--item">
- <v-icon size="small">mdi-map-marker</v-icon>
- {{ request.influencerRegion }}
- </p>
- </div>
- <div class="influencer--meta">
- <span v-if="request.followerCount" class="meta--item">
- <v-icon size="small">mdi-account-group</v-icon>
- {{ formatNumber(request.followerCount) }} 팔로워
- </span>
- <span v-if="request.avgViews" class="meta--item">
- <v-icon size="small">mdi-eye</v-icon>
- 평균 {{ formatNumber(request.avgViews) }} 조회
- </span>
- <span v-if="request.engagementRate" class="meta--item">
- <v-icon size="small">mdi-chart-line</v-icon>
- 참여율 {{ request.engagementRate }}%
- </span>
- </div>
- <div
- v-if="request.influencerDescription"
- class="influencer--description"
- >
- <p>{{ request.influencerDescription }}</p>
- </div>
- <div v-if="request.influencerSnsChannels" class="influencer--sns">
- <div
- v-for="(channel, index) in parseSnsChannels(
- request.influencerSnsChannels
- )"
- :key="index"
- class="sns--item"
- >
- <v-icon size="small">{{ getSnsIcon(channel.platform) }}</v-icon>
- {{ channel.handle }}
- </div>
- </div>
- </div>
- </div>
- <div class="request--status">
- <v-chip :color="getStatusColor(request.STATUS)" size="small">
- {{ getStatusText(request.STATUS) }}
- </v-chip>
- <p class="request--date">{{ formatDate(request.REQUEST_DATE) }}</p>
- </div>
- </div>
- <!-- 카드 바디 -->
- <div class="request--card--body">
- <div v-if="request.REQUEST_MESSAGE" class="request--message">
- <h5>요청 메시지</h5>
- <p>"{{ request.REQUEST_MESSAGE }}"</p>
- </div>
- <div
- v-if="request.COMMISSION_RATE || request.SPECIAL_CONDITIONS"
- class="request--conditions"
- >
- <h5>희망 조건</h5>
- <div v-if="request.COMMISSION_RATE" class="condition--item">
- <span class="condition--label">희망 수수료율:</span>
- <span class="condition--value">{{ request.COMMISSION_RATE }}%</span>
- </div>
- <div v-if="request.SPECIAL_CONDITIONS" class="condition--item">
- <span class="condition--label">특별 조건:</span>
- <span class="condition--value">{{ request.SPECIAL_CONDITIONS }}</span>
- </div>
- </div>
- <div v-if="request.STATUS === 'PENDING'" class="expire--info">
- <v-icon size="16" color="warning">mdi-clock-alert</v-icon>
- <span>만료일: {{ formatDate(request.EXPIRED_DATE) }}</span>
- </div>
- </div>
- <!-- 카드 푸터 (액션 버튼) -->
- <div class="request--card--footer">
- <div class="card--actions">
- <v-btn
- class="custom-btn mini btn-outline"
- @click="viewInfluencerDetail(request.INFLUENCER_SEQ)"
- >
- 프로필 보기
- </v-btn>
- <div v-if="request.STATUS === 'PENDING'" class="approval--actions">
- <v-btn
- class="custom-btn mini btn-red"
- @click="handleReject(request)"
- :loading="processing"
- >
- 거부
- </v-btn>
- <v-btn
- class="custom-btn mini btn-blue"
- @click="handleApprove(request)"
- :loading="processing"
- >
- 승인
- </v-btn>
- </div>
- <div v-else-if="request.STATUS === 'APPROVED'" class="approved--actions">
- <v-btn
- class="custom-btn mini btn-outline"
- @click="viewRequestHistory(request.SEQ)"
- >
- 이력보기
- </v-btn>
- <v-btn
- class="custom-btn mini btn-terminate"
- @click="handleTerminate(request)"
- :loading="processing"
- >
- <v-icon left size="small">mdi-link-off</v-icon>
- 해지
- </v-btn>
- </div>
- <div
- v-else-if="request.STATUS === 'TERMINATED'"
- class="terminated--actions"
- >
- <v-btn
- class="custom-btn mini btn-outline"
- @click="viewRequestHistory(request.SEQ)"
- >
- 이력보기
- </v-btn>
- </div>
- <v-btn
- v-else
- class="custom-btn mini btn-outline"
- @click="viewRequestHistory(request.SEQ)"
- >
- 이력보기
- </v-btn>
- </div>
- </div>
- </div>
- </div>
- <!-- 페이지네이션 -->
- <div class="pagination-wrap" v-if="pagination.totalPages > 1">
- <v-pagination
- v-model="currentPage"
- :length="pagination.totalPages"
- :total-visible="7"
- @update:model-value="handlePageChange"
- ></v-pagination>
- </div>
- </div>
- <!-- 검색 결과 없음 -->
- <div v-else class="no-data-wrap">
- <div class="no-data">
- <v-icon size="64" color="grey-lighten-1">mdi-account-search</v-icon>
- <h3>승인요청이 없습니다</h3>
- <p>아직 인플루언서로부터 승인요청이 없습니다</p>
- </div>
- </div>
- </div>
- <!-- 승인 확인 모달 -->
- <v-dialog v-model="approveModal.show" max-width="500px">
- <v-card>
- <v-card-title class="text-h5 text-success">
- <v-icon left>mdi-check-circle</v-icon>
- 승인 확인
- </v-card-title>
- <v-card-text>
- <div class="approve--content">
- <div class="influencer--summary">
- <div class="influencer--avatar--small">
- <v-img
- v-if="approveModal.request?.influencerAvatar"
- :src="approveModal.request.influencerAvatar"
- width="40"
- height="40"
- ></v-img>
- <div v-else class="no-avatar--small">
- {{ approveModal.request?.influencerNickname?.charAt(0) || "U" }}
- </div>
- </div>
- <div>
- <h4>{{ approveModal.request?.influencerNickname }}</h4>
- <p>{{ getCategoryText(approveModal.request?.influencerCategory) }}</p>
- </div>
- </div>
- <p>이 인플루언서의 승인요청을 승인하시겠습니까?</p>
- <v-textarea
- v-model="approveModal.approveMessage"
- label="승인 메시지 (선택사항)"
- placeholder="인플루언서에게 전달할 메시지를 입력해주세요..."
- rows="3"
- counter="300"
- maxlength="300"
- class="mt-4"
- ></v-textarea>
- </div>
- </v-card-text>
- <v-card-actions>
- <v-spacer></v-spacer>
- <v-btn color="grey" variant="text" @click="closeApproveModal">취소</v-btn>
- <v-btn color="success" @click="confirmApprove" :loading="processing">
- 승인하기
- </v-btn>
- </v-card-actions>
- </v-card>
- </v-dialog>
- <!-- 거부 확인 모달 -->
- <v-dialog v-model="rejectModal.show" max-width="500px">
- <v-card>
- <v-card-title class="text-h5 text-error">
- <v-icon left>mdi-close-circle</v-icon>
- 거부 확인
- </v-card-title>
- <v-card-text>
- <div class="reject--content">
- <div class="influencer--summary">
- <div class="influencer--avatar--small">
- <v-img
- v-if="rejectModal.request?.influencerAvatar"
- :src="rejectModal.request.influencerAvatar"
- width="40"
- height="40"
- ></v-img>
- <div v-else class="no-avatar--small">
- {{ rejectModal.request?.influencerNickname?.charAt(0) || "U" }}
- </div>
- </div>
- <div>
- <h4>{{ rejectModal.request?.influencerNickname }}</h4>
- <p>{{ getCategoryText(rejectModal.request?.influencerCategory) }}</p>
- </div>
- </div>
- <p>이 인플루언서의 승인요청을 거부하시겠습니까?</p>
- <v-textarea
- v-model="rejectModal.rejectReason"
- label="거부 사유"
- placeholder="거부 사유를 입력해주세요..."
- rows="4"
- counter="500"
- maxlength="500"
- class="mt-4"
- required
- ></v-textarea>
- </div>
- </v-card-text>
- <v-card-actions>
- <v-spacer></v-spacer>
- <v-btn color="grey" variant="text" @click="closeRejectModal">취소</v-btn>
- <v-btn color="error" @click="confirmReject" :loading="processing">
- 거부하기
- </v-btn>
- </v-card-actions>
- </v-card>
- </v-dialog>
- <!-- 해지 확인 모달 -->
- <v-dialog v-model="terminateModal.show" max-width="500px">
- <v-card>
- <v-card-title class="text-h5 text-warning">
- <v-icon left>mdi-link-off</v-icon>
- 파트너십 해지 확인
- </v-card-title>
- <v-card-text>
- <div class="terminate--content">
- <div class="influencer--summary">
- <div class="influencer--avatar--small">
- <v-img
- v-if="terminateModal.request?.influencerAvatar"
- :src="terminateModal.request.influencerAvatar"
- width="40"
- height="40"
- ></v-img>
- <div v-else class="no-avatar--small">
- {{ terminateModal.request?.influencerNickname?.charAt(0) || "U" }}
- </div>
- </div>
- <div>
- <h4>{{ terminateModal.request?.influencerNickname }}</h4>
- <p>{{ getCategoryText(terminateModal.request?.influencerCategory) }}</p>
- </div>
- </div>
- <v-alert type="warning" class="mb-4">
- <strong>주의:</strong> 파트너십을 해지하면 협업 관계가 종료되며, 이 작업은
- 되돌릴 수 없습니다.
- </v-alert>
- <p>이 인플루언서와의 파트너십을 해지하시겠습니까?</p>
- <v-textarea
- v-model="terminateModal.terminateReason"
- label="해지 사유"
- placeholder="해지 사유를 입력해주세요..."
- rows="4"
- counter="500"
- maxlength="500"
- class="mt-4"
- required
- ></v-textarea>
- </div>
- </v-card-text>
- <v-card-actions>
- <v-spacer></v-spacer>
- <v-btn color="grey" variant="text" @click="closeTerminateModal">취소</v-btn>
- <v-btn
- class="btn-terminate-confirm"
- @click="confirmTerminate"
- :loading="processing"
- >
- <v-icon left>mdi-link-off</v-icon>
- 해지하기
- </v-btn>
- </v-card-actions>
- </v-card>
- </v-dialog>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, computed } from "vue";
- import { useRouter } from "vue-router";
- /************************************************************************
- | 레이아웃
- ************************************************************************/
- definePageMeta({
- layout: "default",
- });
- /************************************************************************
- | 스토어 & 라우터
- ************************************************************************/
- const router = useRouter();
- const { $toast } = useNuxtApp();
- /************************************************************************
- | 반응형 데이터
- ************************************************************************/
- const pageId = ref("인플루언서 승인요청 관리");
- const loading = ref(false);
- const processing = ref(false);
- const error = ref(null);
- const currentPage = ref(1);
- // 검색 필터
- const searchFilter = ref({
- keyword: "",
- status: "",
- category: "",
- });
- // 정렬 옵션
- const sortOption = ref("latest");
- const sortOptions = ref([
- { title: "최신순", value: "latest" },
- { title: "오래된순", value: "oldest" },
- { title: "마감임박순", value: "expiring" },
- ]);
- // 상태 옵션
- const statusOptions = ref([
- { title: "전체", value: "" },
- { title: "대기중", value: "PENDING" },
- { title: "승인완료", value: "APPROVED" },
- { title: "거부됨", value: "REJECTED" },
- { title: "해지됨", value: "TERMINATED" },
- ]);
- // 카테고리 옵션
- const categoryOptions = ref([
- { title: "전체", value: "" },
- { title: "패션·뷰티", value: "FASHION_BEAUTY" },
- { title: "식품·건강", value: "FOOD_HEALTH" },
- { title: "라이프스타일", value: "LIFESTYLE" },
- { title: "테크·가전", value: "TECH_ELECTRONICS" },
- { title: "스포츠·레저", value: "SPORTS_LEISURE" },
- { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" },
- ]);
- // 데이터
- const requests = ref([]);
- const stats = ref({
- pending: 0,
- approved: 0,
- rejected: 0,
- total: 0,
- });
- const pagination = ref({
- currentPage: 1,
- totalPages: 1,
- totalCount: 0,
- pageSize: 12,
- });
- // 승인 모달
- const approveModal = ref({
- show: false,
- request: null,
- approveMessage: "",
- });
- // 거부 모달
- const rejectModal = ref({
- show: false,
- request: null,
- rejectReason: "",
- });
- // 해지 모달
- const terminateModal = ref({
- show: false,
- request: null,
- terminateReason: "",
- });
- /************************************************************************
- | computed
- ************************************************************************/
- const currentUser = computed(() => {
- const authData = JSON.parse(localStorage.getItem("authStore"))?.auth || {};
- console.log("🔍 currentUser (벤더 대시보드):", authData);
- return authData;
- });
- /************************************************************************
- | 메서드
- ************************************************************************/
- const handleSearch = async () => {
- currentPage.value = 1;
- await loadRequests();
- };
- const handlePageChange = async (page) => {
- currentPage.value = page;
- await loadRequests();
- };
- const handleSort = async () => {
- currentPage.value = 1;
- await loadRequests();
- };
- const loadRequests = async () => {
- try {
- loading.value = true;
- error.value = null;
- const params = {
- vendorSeq: currentUser.value.seq,
- keyword: searchFilter.value.keyword,
- status: searchFilter.value.status,
- category: searchFilter.value.category,
- sortBy: sortOption.value,
- page: currentPage.value,
- size: pagination.value.pageSize,
- };
- console.log("🔍 loadRequests 호출됨:", params);
- useAxios()
- .post("/api/vendor-influencer/requests", params)
- .then((res) => {
- console.log("📥 API 응답:", res.data);
- if (res.data.success) {
- const items = res.data.data.items;
- console.log("📋 받아온 요청 목록:", items.length, items);
- // SEQ 중복 확인
- const seqs = items.map((item) => item.SEQ);
- const uniqueSeqs = [...new Set(seqs)];
- if (seqs.length !== uniqueSeqs.length) {
- console.warn("⚠️ 중복된 SEQ 발견:", seqs);
- }
- requests.value = items;
- pagination.value = res.data.data.pagination;
- stats.value = res.data.data.stats || stats.value;
- } else {
- error.value =
- res.data.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
- }
- })
- .catch((err) => {
- error.value = err.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
- })
- .finally(() => {
- loading.value = false;
- });
- } catch (err) {
- error.value = err.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
- loading.value = false;
- }
- };
- const handleApprove = (request) => {
- approveModal.value = {
- show: true,
- request: request,
- approveMessage: "",
- };
- };
- const closeApproveModal = () => {
- approveModal.value = {
- show: false,
- request: null,
- approveMessage: "",
- };
- };
- const confirmApprove = async () => {
- try {
- processing.value = true;
- const params = {
- mappingSeq: approveModal.value.request.SEQ,
- action: "APPROVE",
- processedBy: currentUser.value.seq,
- responseMessage: approveModal.value.approveMessage,
- };
- console.log("✅ 승인 처리 시작:", params);
- useAxios()
- .post("/api/vendor-influencer/approve", params)
- .then((res) => {
- console.log("📥 승인 처리 응답:", res.data);
- if (res.data.success) {
- $toast.success("승인요청이 승인되었습니다.");
- closeApproveModal();
- console.log("🔄 승인 후 목록 새로고침");
- loadRequests();
- } else {
- console.error("❌ 승인 처리 실패:", res.data);
- $toast.error(res.data.message || "승인 처리 중 오류가 발생했습니다.");
- }
- })
- .catch((err) => {
- $toast.error(err.message || "승인 처리 중 오류가 발생했습니다.");
- })
- .finally(() => {
- processing.value = false;
- });
- } catch (err) {
- $toast.error(err.message || "승인 처리 중 오류가 발생했습니다.");
- processing.value = false;
- }
- };
- const handleReject = (request) => {
- rejectModal.value = {
- show: true,
- request: request,
- rejectReason: "",
- };
- };
- const closeRejectModal = () => {
- rejectModal.value = {
- show: false,
- request: null,
- rejectReason: "",
- };
- };
- const confirmReject = async () => {
- if (!rejectModal.value.rejectReason.trim()) {
- $toast.error("거부 사유를 입력해주세요.");
- return;
- }
- try {
- processing.value = true;
- const params = {
- mappingSeq: rejectModal.value.request.SEQ,
- action: "REJECT",
- processedBy: currentUser.value.seq,
- responseMessage: rejectModal.value.rejectReason,
- };
- useAxios()
- .post("/api/vendor-influencer/approve", params)
- .then((res) => {
- if (res.data.success) {
- $toast.success("승인요청이 거부되었습니다.");
- closeRejectModal();
- loadRequests();
- } else {
- $toast.error(res.data.message || "거부 처리 중 오류가 발생했습니다.");
- }
- })
- .catch((err) => {
- $toast.error(err.message || "거부 처리 중 오류가 발생했습니다.");
- })
- .finally(() => {
- processing.value = false;
- });
- } catch (err) {
- $toast.error(err.message || "거부 처리 중 오류가 발생했습니다.");
- processing.value = false;
- }
- };
- const viewInfluencerDetail = (influencerSeq) => {
- router.push(`/view/influencer/${influencerSeq}`);
- };
- const viewRequestHistory = (requestSeq) => {
- router.push(`/view/vendor/request-history/${requestSeq}`);
- };
- /**
- * 파트너십 해지
- */
- const handleTerminate = (request) => {
- terminateModal.value = {
- show: true,
- request: request,
- terminateReason: "",
- };
- };
- const closeTerminateModal = () => {
- terminateModal.value = {
- show: false,
- request: null,
- terminateReason: "",
- };
- };
- const confirmTerminate = async () => {
- if (!terminateModal.value.terminateReason.trim()) {
- $toast.error("해지 사유를 입력해주세요.");
- return;
- }
- try {
- processing.value = true;
- const params = {
- mappingSeq: terminateModal.value.request.SEQ,
- terminateReason: terminateModal.value.terminateReason,
- terminatedBy: currentUser.value.seq,
- };
- console.log("🔗 파트너십 해지 처리 시작:", params);
- useAxios()
- .post("/api/vendor-influencer/terminate", params)
- .then((res) => {
- console.log("📥 해지 처리 응답:", res.data);
- if (res.data.success) {
- $toast.success("파트너십이 해지되었습니다.");
- closeTerminateModal();
- console.log("🔄 해지 후 목록 새로고침");
- loadRequests();
- } else {
- console.error("❌ 해지 처리 실패:", res.data);
- $toast.error(res.data.message || "해지 처리 중 오류가 발생했습니다.");
- }
- })
- .catch((err) => {
- $toast.error(err.message || "해지 처리 중 오류가 발생했습니다.");
- })
- .finally(() => {
- processing.value = false;
- });
- } catch (err) {
- $toast.error(err.message || "해지 처리 중 오류가 발생했습니다.");
- processing.value = false;
- }
- };
- // 유틸리티 함수들
- const getCategoryText = (category) => {
- const categoryMap = {
- FASHION_BEAUTY: "패션·뷰티",
- FOOD_HEALTH: "식품·건강",
- LIFESTYLE: "라이프스타일",
- TECH_ELECTRONICS: "테크·가전",
- SPORTS_LEISURE: "스포츠·레저",
- CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
- };
- return categoryMap[category] || category || "기타";
- };
- const getStatusText = (status) => {
- const statusMap = {
- PENDING: "대기중",
- APPROVED: "승인완료",
- REJECTED: "거절됨",
- TERMINATED: "해지됨",
- EXPIRED: "만료됨",
- };
- return statusMap[status] || status || "알 수 없음";
- };
- const getStatusColor = (status) => {
- const colorMap = {
- PENDING: "orange",
- APPROVED: "success",
- REJECTED: "error",
- TERMINATED: "warning",
- EXPIRED: "grey",
- };
- return colorMap[status] || "grey";
- };
- const getRequestStatusClass = (status) => {
- return `request-status-${status?.toLowerCase() || "unknown"}`;
- };
- const formatDate = (dateString) => {
- return new Date(dateString).toLocaleDateString("ko-KR");
- };
- const formatNumber = (num) => {
- if (!num) return "0";
- if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
- if (num >= 1000) return (num / 1000).toFixed(1) + "K";
- return num.toString();
- };
- const parseSnsChannels = (snsChannels) => {
- try {
- return JSON.parse(snsChannels);
- } catch (e) {
- return [];
- }
- };
- const getSnsIcon = (platform) => {
- const iconMap = {
- instagram: "mdi-instagram",
- youtube: "mdi-youtube",
- tiktok: "mdi-music-note",
- blog: "mdi-post",
- facebook: "mdi-facebook",
- twitter: "mdi-twitter",
- };
- return iconMap[platform.toLowerCase()] || "mdi-link";
- };
- /************************************************************************
- | 라이프사이클
- ************************************************************************/
- onMounted(async () => {
- console.log("🚀 influencer-requests 컴포넌트 마운트됨");
- await loadRequests();
- });
- </script>
- <style scoped>
- .stats--cards--wrap {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
- }
- .stats--card {
- background: white;
- border-radius: 12px;
- padding: 20px;
- display: flex;
- align-items: center;
- gap: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- .stats--icon {
- width: 50px;
- height: 50px;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- }
- .stats--icon.pending {
- background: #ff9800;
- }
- .stats--icon.approved {
- background: #4caf50;
- }
- .stats--icon.rejected {
- background: #f44336;
- }
- .stats--icon.total {
- background: #2196f3;
- }
- .stats--content h3 {
- margin: 0;
- font-size: 24px;
- font-weight: 700;
- color: #333;
- }
- .stats--content p {
- margin: 0;
- font-size: 14px;
- color: #666;
- }
- .requests--list--wrap {
- margin-top: 20px;
- }
- .requests--grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
- gap: 20px;
- margin-bottom: 20px;
- }
- .request--card {
- background: white;
- border-radius: 12px;
- padding: 20px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- transition: transform 0.2s, box-shadow 0.2s;
- border-left: 4px solid #e0e0e0;
- }
- .request--card:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
- }
- .request--card.request-status-pending {
- border-left-color: #ff9800;
- }
- .request--card.request-status-approved {
- border-left-color: #4caf50;
- }
- .request--card.request-status-rejected {
- border-left-color: #f44336;
- }
- .request--card.request-status-terminated {
- border-left-color: #ff9800;
- }
- .request--card--header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 16px;
- }
- .influencer--info {
- display: flex;
- gap: 12px;
- flex: 1;
- }
- .influencer--avatar {
- width: 50px;
- height: 50px;
- border-radius: 50%;
- overflow: hidden;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #f5f5f5;
- }
- .no-avatar {
- font-size: 20px;
- font-weight: bold;
- color: #666;
- }
- .influencer--details h4 {
- margin: 0 0 4px 0;
- font-size: 16px;
- font-weight: 600;
- }
- .influencer--category {
- color: #666;
- font-size: 14px;
- margin: 0 0 8px 0;
- }
- .influencer--meta {
- display: flex;
- flex-direction: column;
- gap: 2px;
- }
- .influencer--meta span {
- font-size: 12px;
- color: #888;
- }
- .request--status {
- text-align: right;
- flex-shrink: 0;
- }
- .request--date {
- margin: 8px 0 0;
- font-size: 12px;
- color: #999;
- }
- .request--card--body {
- margin-bottom: 16px;
- }
- .request--message,
- .request--conditions {
- margin-bottom: 12px;
- }
- .request--message h5,
- .request--conditions h5 {
- margin: 0 0 8px 0;
- font-size: 14px;
- font-weight: 600;
- color: #333;
- }
- .request--message p {
- margin: 0;
- font-size: 14px;
- color: #666;
- font-style: italic;
- }
- .condition--item {
- display: flex;
- gap: 8px;
- margin-bottom: 4px;
- }
- .condition--label {
- font-size: 13px;
- color: #666;
- min-width: 80px;
- }
- .condition--value {
- font-size: 13px;
- color: #333;
- font-weight: 500;
- }
- .expire--info {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- color: #ff9800;
- background: #fff8e1;
- padding: 6px 10px;
- border-radius: 6px;
- }
- .request--card--footer {
- border-top: 1px solid #f0f0f0;
- padding-top: 16px;
- }
- .card--actions {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .approval--actions,
- .approved--actions,
- .terminated--actions {
- display: flex;
- gap: 8px;
- }
- .loading-wrap,
- .error-wrap,
- .no-data-wrap {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60px 20px;
- }
- .no-data {
- text-align: center;
- }
- .no-data h3 {
- margin: 16px 0 8px;
- color: #666;
- }
- .no-data p {
- color: #999;
- }
- .pagination-wrap {
- display: flex;
- justify-content: center;
- margin-top: 20px;
- }
- .approve--content,
- .reject--content,
- .terminate--content {
- padding: 8px 0;
- }
- .influencer--summary {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px;
- background: #f8f9fa;
- border-radius: 8px;
- margin-bottom: 16px;
- }
- .influencer--avatar--small {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- overflow: hidden;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #f5f5f5;
- }
- .no-avatar--small {
- font-size: 16px;
- font-weight: bold;
- color: #666;
- }
- .result-count {
- font-size: 14px;
- color: #666;
- font-weight: 500;
- }
- .influencer--contact {
- margin: 4px 0;
- }
- .contact--item {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 13px;
- color: #666;
- margin: 2px 0;
- }
- .contact--item .v-icon {
- color: #999;
- }
- .influencer--header {
- margin-bottom: 8px;
- }
- .influencer--contact {
- margin: 8px 0;
- padding: 8px;
- background: #f8f9fa;
- border-radius: 6px;
- }
- .contact--item {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 13px;
- color: #666;
- margin: 4px 0;
- }
- .influencer--meta {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- margin: 8px 0;
- }
- .meta--item {
- display: flex;
- align-items: center;
- gap: 4px;
- font-size: 13px;
- color: #555;
- background: #f0f0f0;
- padding: 4px 8px;
- border-radius: 4px;
- }
- .influencer--description {
- margin: 8px 0;
- font-size: 13px;
- color: #666;
- line-height: 1.4;
- }
- .influencer--sns {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-top: 8px;
- }
- .sns--item {
- display: flex;
- align-items: center;
- gap: 4px;
- font-size: 12px;
- color: #555;
- background: #eef2ff;
- padding: 4px 8px;
- border-radius: 4px;
- }
- /* 해지 버튼 전용 스타일 */
- .btn-terminate {
- background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
- color: white;
- border: none;
- font-weight: 700;
- min-width: 100px;
- padding: 8px 16px;
- box-shadow: 0 4px 12px rgba(229, 62, 62, 0.3);
- transition: all 0.3s ease;
- }
- .btn-terminate:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(229, 62, 62, 0.4);
- background: linear-gradient(135deg, #c53030 0%, #9b2c2c 100%);
- }
- .btn-terminate:active {
- transform: translateY(0);
- }
- .btn-terminate:disabled {
- background: #e2e8f0;
- color: #a0aec0;
- box-shadow: none;
- transform: none !important;
- }
- .btn-green {
- background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
- color: white;
- border: none;
- font-weight: 600;
- min-width: 140px;
- padding: 8px 16px;
- box-shadow: 0 4px 12px rgba(56, 161, 105, 0.3);
- transition: all 0.3s ease;
- }
- .btn-green:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(56, 161, 105, 0.4);
- background: linear-gradient(135deg, #2f855a 0%, #276749 100%);
- }
- .btn-green:active {
- transform: translateY(0);
- }
- .btn-green:disabled {
- background: #e6fffa;
- color: #38a169;
- border: 1px solid #38a169;
- box-shadow: none;
- transform: none !important;
- opacity: 0.7;
- }
- /* 해지 확인 모달 버튼 스타일 */
- .btn-terminate-confirm {
- background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
- color: white !important;
- font-weight: 700 !important;
- font-size: 14px !important;
- padding: 12px 24px !important;
- border-radius: 8px !important;
- border: none !important;
- box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4) !important;
- transition: all 0.3s ease !important;
- text-transform: none !important;
- letter-spacing: 0.5px !important;
- min-width: 120px !important;
- }
- .btn-terminate-confirm:hover {
- background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%) !important;
- box-shadow: 0 6px 16px rgba(220, 38, 38, 0.5) !important;
- transform: translateY(-2px) !important;
- }
- .btn-terminate-confirm:active {
- transform: translateY(0) !important;
- box-shadow: 0 3px 8px rgba(220, 38, 38, 0.4) !important;
- }
- .btn-terminate-confirm .v-icon {
- color: white !important;
- margin-right: 6px !important;
- }
- .btn-terminate-confirm:disabled {
- background: #fca5a5 !important;
- color: #9ca3af !important;
- box-shadow: none !important;
- transform: none !important;
- }
- </style>
|