| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492 |
- <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 && 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.CURRENT_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.INFLUENCER_NICKNAME || request.INFLUENCER_NAME }}</h4>
- <p class="influencer--category">
- {{ getCategoryText(request.influencerCategory) }}
- </p>
- </div>
- <div class="influencer--contact">
- <p v-if="request.INFLUENCER_EMAIL" class="contact--item">
- <v-icon size="small">mdi-email</v-icon>
- {{ request.INFLUENCER_EMAIL }}
- </p>
- <p v-if="request.INFLUENCER_PHONE" class="contact--item">
- <v-icon size="small">mdi-phone</v-icon>
- {{ request.INFLUENCER_PHONE }}
- </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">
- <div class="status--badges">
- <v-chip :color="getStatusColor(request.CURRENT_STATUS)" size="small">
- {{ getStatusText(request.CURRENT_STATUS) }}
- </v-chip>
- <v-chip
- v-if="request.ADD_INFO1 === 'REAPPLY'"
- color="orange"
- size="small"
- class="ml-2"
- >
- 재승인요청
- </v-chip>
- </div>
- <p class="request--date">{{ formatDate(request.REQUEST_DATE) }}</p>
- </div>
- </div>
- <!-- 카드 바디 -->
- <div class="request--card--body">
- <!-- 재승인 요청 안내 -->
- <div v-if="request.ADD_INFO1 === 'REAPPLY'" class="reapply--notice">
- <v-alert
- type="info"
- variant="tonal"
- density="compact"
- class="mb-4"
- >
- <v-icon size="16">mdi-refresh</v-icon>
- <span class="ml-2">
- 이전에 파트너십을 맺었던 인플루언서의 재승인 요청입니다.
- <br>이전 파트너십 종료일: {{ formatDate(request.ADD_INFO3) }}
- </span>
- </v-alert>
- </div>
- <div v-if="request.REQUEST_MESSAGE" class="request--message">
- <h5>{{ request.ADD_INFO1 === 'REAPPLY' ? '재승인 요청 메시지' : '요청 메시지' }}</h5>
- <p>"{{ request.REQUEST_MESSAGE }}"</p>
- </div>
- <div class="request--commission">
- <h5>수수료 조건</h5>
- <p>{{ request.COMMISSION_RATE || 0 }}%</p>
- </div>
- <div v-if="request.SPECIAL_CONDITIONS" class="request--conditions">
- <h5>특별 조건</h5>
- <p>"{{ request.SPECIAL_CONDITIONS }}"</p>
- </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.CURRENT_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"
- >
- {{ request.ADD_INFO1 === 'REAPPLY' ? '재승인' : '승인' }}
- </v-btn>
- </div>
- <div v-else-if="request.CURRENT_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.CURRENT_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 { computed, onMounted, ref } 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(() => {
- try {
- const authStore = localStorage.getItem("authStore");
- if (!authStore) {
- console.warn("⚠️ authStore가 localStorage에 없습니다");
- return {};
- }
- const parsedStore = JSON.parse(authStore);
- const authData = parsedStore?.auth || {};
-
- console.log("🔍 localStorage authStore:", parsedStore);
- console.log("🔍 currentUser (벤더 대시보드):", authData);
-
- // seq 필드가 없으면 다른 가능한 필드들 시도
- if (!authData.seq) {
- authData.seq = authData.SEQ || authData.id || authData.user_seq || authData.userSeq;
- console.log("🔧 seq 필드 보정:", authData.seq);
- }
-
- return authData;
- } catch (error) {
- console.error("❌ authStore 파싱 오류:", error);
- return {};
- }
- });
- /************************************************************************
- | 메서드
- ************************************************************************/
- 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 중복 확인
- if (items.length > 0) {
- 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 = {
- totalCount: res.data.data.total || 0,
- currentPage: res.data.data.page || 1,
- totalPages: res.data.data.totalPages || 1,
- pageSize: res.data.data.size || 20
- };
- stats.value = res.data.data.stats || {
- pending: 0,
- approved: 0,
- rejected: 0,
- total: 0
- };
- } 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("🔍 현재 사용자 정보:", currentUser.value);
- console.log("🔍 processedBy 값:", currentUser.value.seq);
- 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(3, 1fr);
- gap: 20px;
- margin-bottom: 20px;
- }
-
- @media (max-width: 1200px) {
- .requests--grid {
- grid-template-columns: repeat(2, 1fr);
- }
- }
-
- @media (max-width: 768px) {
- .requests--grid {
- grid-template-columns: 1fr;
- }
- }
- .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--card--body .reapply--notice .text-info {
- font-size: 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;
- }
- .status--badges {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
- }
- .status--badges .v-chip {
- font-weight: 500;
- }
- </style>
|