diff --git a/docs/20260413_에이전트소속크리에이터목록.md b/docs/20260413_에이전트소속크리에이터목록.md new file mode 100644 index 0000000..f23905d --- /dev/null +++ b/docs/20260413_에이전트소속크리에이터목록.md @@ -0,0 +1,90 @@ +# 에이전트 소속 크리에이터 목록 페이지 구현 계획 + +- [ ] API 호출 함수 추가 (`src/api/agent_calculate.js`) +- [ ] 페이지 테이블 구현 (`src/views/Agent/Creators.vue`) +- [ ] 서버 페이징 연동 (page, size, totalCount) +- [ ] 기본 로딩/에러 처리 +- [ ] 테이블 화면 중앙 정렬 및 내용 크기 기반 축소 표시 (`src/views/Agent/Creators.vue`) + - [ ] 프로필 이미지/테이블 2배 확대 (`src/views/Agent/Creators.vue`) + - [ ] v-pagination으로 커스텀 페이징 적용 (`src/views/Agent/Creators.vue`) + - [ ] 순번 컬럼 추가(역순, 페이지 연속) (`src/views/Agent/Creators.vue`) + +## 검증 기록 + +### 1차 구현 +- 무엇을: 에이전트 소속 크리에이터 목록 조회 및 테이블 렌더링, 페이지 전환 확인 +- 왜: 에이전트가 담당 크리에이터 정보를 빠르게 확인할 수 있도록 하기 위함 +- 어떻게: + - 애플리케이션 실행 후 좌측 메뉴에서 해당 페이지 진입 + - 1페이지 로드 시 닉네임/프로필 이미지 표시 여부 확인 (성공/실패) + - 페이지네이션에서 2페이지로 이동하여 데이터 갱신 확인 (성공/실패) + - 실패 시 개발자 도구 네트워크 탭으로 요청/응답 확인 후 사유 기록 + +### 2차 수정 (UI 정렬/크기) +- 무엇을: 테이블을 화면 가득이 아닌 가운데 정렬하고, 내용 크기에 맞춰 축소되도록 스타일 조정 +- 왜: 가독성 향상 및 과도한 공백 제거 요청 반영 +- 어떻게: + - `src/views/Agent/Creators.vue`에서 ``, ``로 컨테이너 폭 제한 + - `v-data-table`에 `shrink-table` 클래스 적용하여 `display: inline-table; width: auto;`로 내용 기반 너비 설정 + - 검증: 브라우저에서 페이지 진입 후 테이블이 가운데 정렬되고 좌우로 과도하게 늘어나지 않는지 확인 (성공/실패) + +### 3차 수정 (데이터 미표시 해결 + v-pagination 적용) +- 무엇을: API 응답 파싱 보강으로 목록 데이터 미표시 이슈 해결, `v-pagination` 기반 페이징 적용 +- 왜: 일부 API가 `{ success, data }` 래핑 형태로 응답하여 기존 파싱 로직에서 `items`가 세팅되지 않던 문제 해결 및 요구사항에 따른 커스텀 페이징 적용 +- 어떻게: + - `src/views/Agent/Creators.vue` + - `fetchItems`에서 `success === true && data`, `data` 단독, 래핑 없음까지 모두 대응하도록 분기처리 추가 + - `total_page = ceil(totalCount / itemsPerPage)` 계산 및 `v-pagination` 추가(@input → `fetchItems`) + - `page` watcher 제거로 중복 호출 방지, 오류 시 `notifyError`로 안내 + - 검증(수동): + - 페이지 진입 시 1페이지 데이터가 테이블에 표시되는지 확인 (성공/실패) + - `v-pagination`에서 2페이지 클릭 시 새 데이터로 갱신 및 로딩 인디케이터 동작 확인 (성공/실패) + - 네트워크 탭에서 `/agent/calculate/creator/list?page=1&size=20` 요청/응답 확인 (성공/실패) + +### 4차 수정 (프로필 이미지 및 테이블 크기 2배) +- 무엇을: 프로필 이미지 아바타 크기를 36 → 72로 2배 확대, 테이블 표시 크기를 2배로 스케일링 +- 왜: 목록 가독성 향상 및 요구사항(2배 확대) 반영 +- 어떻게: + - `src/views/Agent/Creators.vue` + - ``로 변경 + - `.shrink-table`에 `transform: scale(2); transform-origin: top center;` 추가하여 렌더 크기 2배 확대 + - 검증(수동): + - 페이지 진입 시 아바타 직경이 기존 대비 2배로 커졌는지 확인 (성공/실패) + - 테이블 텍스트/셀 간격 등 전체 렌더가 2배로 커져 중앙에 표시되는지 확인 (성공/실패) + +### 5차 수정 (아이템 패딩 추가 + 헤더 가운데 정렬) +- 무엇을: 테이블 아이템(셀) 패딩을 10px 추가하여 행 간격 확보, 헤더 텍스트 가운데 정렬 +- 왜: 아이템 사이 공간이 없어 답답하다는 피드백 반영 및 가독성 향상 +- 어떻게: + - `src/views/Agent/Creators.vue` + - headers `align: 'center'`로 업데이트(프로필/닉네임) + - `.shrink-table ::v-deep .v-data-table__wrapper table tbody tr > td { padding: 10px; }` 추가 + - 보조로 `.shrink-table ::v-deep thead th { text-align: center !important; }` 추가 + - 검증(수동): + - 테이블 행의 세로/가로 여백이 이전 대비 확장되었는지 확인 (성공/실패) + - 헤더 텍스트가 가운데 정렬로 표시되는지 확인 (성공/실패) + +### 6차 수정 (셀 패딩 미적용 해결 + 테이블 너비 50%) +- 무엇을: 셀(td) 패딩이 적용되지 않던 문제를 deep 선택자/우선순위로 해결, 테이블 너비를 내용 기반(auto)에서 화면 50%로 변경 +- 왜: Vuetify 기본 스타일 우선 적용으로 padding 반영 실패, 내용 기반 너비가 너무 작아 가독성 저하 +- 어떻게: + - `src/views/Agent/Creators.vue` + - `.shrink-table ::v-deep .v-data-table__wrapper > table > tbody > tr > td { padding: 10px !important; }`로 선택자 보강 및 `!important` 적용 + - `.shrink-table { display: block; width: 50vw; }`로 변경하여 중앙 정렬 상태에서 화면의 50% 너비 유지 + - 기존 `transform: scale(2)`는 제거하여 너비 계산 왜곡 방지(아바타 72px 확대는 유지) + - 검증(수동): + - 각 셀에 10px 패딩이 실제로 적용되는지 확인(개발자 도구로 td 컴퓨티드 스타일 확인) (성공/실패) + - 브라우저 폭 기준 테이블이 약 50% 너비를 차지하고 중앙에 정렬되는지 확인 (성공/실패) + - 창 크기를 조절해도 50% 비율이 유지되는지 확인 (성공/실패) + +### 7차 수정 (순번 컬럼 역순/페이지 연속) +- 무엇을: 테이블 맨 앞에 순번 컬럼을 추가하고 전체 인원 기준 역순으로 번호를 표시. 페이지가 넘어가도 연속되도록 구현 +- 왜: 많은 인원 목록에서 상대적 위치를 직관적으로 파악하기 위함 +- 어떻게: + - `src/views/Agent/Creators.vue` + - headers에 `{ text: '순번', value: 'no', align: 'center', sortable: false }` 추가 + - `fetchItems` 완료 후 `no = totalCount - ((page - 1) * itemsPerPage) - index` 로 각 아이템에 번호 주입 + - 검증(수동): + - 1페이지에서 첫 행의 순번이 `totalCount`와 동일하고, 아래로 갈수록 1씩 감소하는지 확인 (성공/실패) + - 2페이지로 이동 시 이전 페이지 마지막 다음 번호가 이어서 표시되는지 확인 (성공/실패) + - 마지막 페이지 마지막 행의 번호가 1인지 확인 (성공/실패) diff --git a/docs/20260413_에이전트용메뉴추가.md b/docs/20260413_에이전트용메뉴추가.md new file mode 100644 index 0000000..b8f926d --- /dev/null +++ b/docs/20260413_에이전트용메뉴추가.md @@ -0,0 +1,25 @@ +# 에이전트용 메뉴 추가 (기본 메뉴 생성) + +- [x] SideMenu에서 getMenus 결과가 빈 배열일 때 기본 메뉴로 대체한다. + - [x] 기본 메뉴 항목: `{ title: '소속 크리에이터', route: '/agent/creators' }` +- [x] 라우터에 `/agent/creators` 경로를 추가한다. (DefaultLayout 하위) +- [x] 페이지 컴포넌트를 생성한다. (`src/views/Agent/Creators.vue`), 내용은 placeholder로 둔다. + +## 배경/의도 +- 에이전트용 메뉴가 아직 백엔드에 없어서 메뉴 조회 시 빈 배열이 내려올 가능성이 높다. +- 빈 배열일 때는 로그아웃 처리 대신 기본 단독 메뉴 ‘소속 크리에이터’를 제공해 접근할 수 있도록 한다. + +## 구현 체크리스트 +- [x] `SideMenu.vue`의 `getMenus()`에서 성공(200/success)이고 `data.length === 0`이면 기본 메뉴로 할당 +- [x] 기존처럼 성공 + 데이터 존재 시에는 응답 데이터를 그대로 사용 +- [x] 라우터 `index.js`에 `/agent/creators` children route 추가 +- [x] `src/views/Agent/Creators.vue` 생성 및 라우터 연동 + +## 검증 기록 +### 1차 구현 +- 무엇을: 빈 메뉴 응답 시 기본 메뉴 노출 및 라우팅 동작 확인 +- 왜: 에이전트용 메뉴가 없어도 최소 탐색 경로를 제공하기 위함 +- 어떻게: + - 가정: API 응답이 `{ success: true, data: [] }` + - 기대: 사이드 메뉴에 ‘소속 크리에이터’ 단일 항목 표시, 클릭 시 `/agent/creators`로 이동하여 placeholder 화면 노출 + - 결과: 로컬에서 메뉴가 빈 배열일 때 사이드 메뉴에 ‘소속 크리에이터’가 표시되고 클릭 시 `/agent/creators`로 이동하여 placeholder 화면이 노출됨을 확인함(수동 확인) diff --git a/docs/20260413_에이전트정산페이지구현.md b/docs/20260413_에이전트정산페이지구현.md new file mode 100644 index 0000000..cbf6dd8 --- /dev/null +++ b/docs/20260413_에이전트정산페이지구현.md @@ -0,0 +1,41 @@ +# 에이전트 전용 정산 페이지 구현 계획 + +## 목적 +- 에이전트 전용 크리에이터별 정산 페이지 5종을 구현한다. +- 날짜 범위 지정 UI 제공, 합계를 테이블 최상단에 노출, 서버 페이징 대응. + +## 대상 페이지 +- 라이브 정산: `/agent/calculate/live` +- 콘텐츠 정산: `/agent/calculate/content-by-date` +- 콘텐츠 후원 정산: `/agent/calculate/content-donation-by-date` +- 커뮤니티 정산: `/agent/calculate/community-post` +- 채널 후원 정산: `/agent/calculate/channel-donation` + +## API +- Method: GET +- Base URL prefix: `/agent/calculate/*` +- 공통 쿼리: `startDateStr`, `endDateStr`, `page`, `size` +- 응답 스키마(공통): + - `totalCount: number` + - `total: { count, totalCan, krw, fee, settlementAmount, tax, depositAmount, agentSettlementAmount }` + - `items: Array<{ creatorId, creatorNickname, count, totalCan, krw, fee, settlementAmount, tax, depositAmount, agentSettlementAmount }>` + +## 체크리스트 +- [x] API 모듈 `src/api/agent_calculate.js` 생성 및 5개 함수 구현 +- [x] 날짜 범위 선택 UI 및 조회 버튼 구현(공통 패턴 사용: vuejs-datetimepicker) +- [x] 데이터 테이블 구현(헤더: 닉네임, 건수, 총 CAN, 원화, 수수료, 정산금액, 세금, 입금액, 에이전트 정산금액) +- [x] 합계(total)를 테이블 최상단 body.prepend로 표시 +- [x] 서버 페이징(v-pagination) 연동 및 총 페이지 계산 +- [x] 로딩 인디케이터/에러 노티 추가 +- [x] 각 페이지 초기 로드 시 당월 1일 ~ 말일 기본 조회 + +## 검증 기록 +### 1차 구현 +- 무엇을: 에이전트 전용 정산 5개 화면(API 연동 훅/합계/페이징/숫자 포맷) 구현 +- 왜: 에이전트가 기간별 크리에이터 정산 현황을 통합 조회하기 위함 +- 어떻게: + - 명령/절차: 로컬 서버 실행 후 각 경로 진입 → 기본 기간(당월) 자동 조회 → 날짜 변경 후 조회 → 합계/목록/페이징 확인 + - 결과: UI 및 파라미터 구성 확인 완료. 백엔드 연결 환경에서 200 응답 시 데이터 표시 예상(수동 점검 예정) + +## 정정 +- 현재 없음. 추후 백엔드 스펙 변경 시 쿼리/필드명 반영 예정. diff --git a/docs/20260413_정산메뉴에이전트전용분리.md b/docs/20260413_정산메뉴에이전트전용분리.md new file mode 100644 index 0000000..aaba50b --- /dev/null +++ b/docs/20260413_정산메뉴에이전트전용분리.md @@ -0,0 +1,29 @@ +# 정산 메뉴 에이전트 전용 분리 계획 + +## 배경/목표 +- 기존 `/calculate/*` 경로는 크리에이터 전용 라우트/페이지다. +- 빈 메뉴일 때 기본으로 추가한 정산 메뉴 5종은 에이전트 전용으로 분리되어야 한다. +- 에이전트 전용 라우트를 `/agent/calculate/*` 네임스페이스로 제공하고, 페이지는 추후 구현을 위해 플레이스홀더로 생성한다. + +## 구현 체크리스트 +- [x] 라우터에 에이전트 전용 경로 추가 (`/agent/calculate/*`) + - [x] `/agent/calculate/live` + - [x] `/agent/calculate/content-by-date` + - [x] `/agent/calculate/content-donation-by-date` + - [x] `/agent/calculate/community-post` + - [x] `/agent/calculate/channel-donation` +- [x] 에이전트 전용 뷰 컴포넌트(플레이스홀더) 생성: `src/views/Agent/Calculate/*` +- [x] 사이드 메뉴 기본 항목이 에이전트 전용 경로를 가리키도록 수정 +- [x] 기본 동작 수동 검증 (라우팅/메뉴 이동 확인) + +## 범위/제한 +- API 연동은 포함하지 않음. 화면은 "추후 구현" 플레이스홀더로 둔다. +- 기존 크리에이터 전용 라우트/화면은 변경하지 않는다. + +## 검증 기록 +### 1차 구현 +- 무엇을: 에이전트 전용 라우트 5종 추가 및 사이드 메뉴 경로 교체, 플레이스홀더 화면 생성 +- 왜: 크리에이터 전용 경로와 구분하여 접근/권한/콘텍스트 분리를 명확히 하기 위함 +- 어떻게: + - 라우팅 수동 점검: 각 메뉴 클릭 시 `/agent/calculate/*` 로 이동되는지 확인 + - 결과: 성공(플레이스홀더 화면 타이틀 확인) diff --git a/src/api/agent_calculate.js b/src/api/agent_calculate.js new file mode 100644 index 0000000..62e5a83 --- /dev/null +++ b/src/api/agent_calculate.js @@ -0,0 +1,53 @@ +import Vue from 'vue'; + +// 에이전트 전용 정산 API + +async function getAgentAssignedCreatorList(page, size) { + return Vue.axios.get( + '/agent/calculate/creator/list?page=' + (page - 1) + '&size=' + size + ); +} + +async function getLiveByCreator(startDate, endDate, page, size) { + return Vue.axios.get( + '/agent/calculate/live-by-creator?startDateStr=' + startDate + + '&endDateStr=' + endDate + '&page=' + (page - 1) + '&size=' + size + ); +} + +async function getContentByCreator(startDate, endDate, page, size) { + return Vue.axios.get( + '/agent/calculate/content-by-creator?startDateStr=' + startDate + + '&endDateStr=' + endDate + '&page=' + (page - 1) + '&size=' + size + ); +} + +async function getContentDonationByCreator(startDate, endDate, page, size) { + return Vue.axios.get( + '/agent/calculate/content-donation-by-creator?startDateStr=' + startDate + + '&endDateStr=' + endDate + '&page=' + (page - 1) + '&size=' + size + ); +} + +async function getCommunityByCreator(startDate, endDate, page, size) { + return Vue.axios.get( + '/agent/calculate/community-by-creator?startDateStr=' + startDate + + '&endDateStr=' + endDate + '&page=' + (page - 1) + '&size=' + size + ); +} + +async function getChannelDonationByCreator(startDate, endDate, page, size) { + return Vue.axios.get( + '/agent/calculate/channel-donation-by-creator?startDateStr=' + startDate + + '&endDateStr=' + endDate + '&page=' + (page - 1) + '&size=' + size + ); +} + +export { + getAgentAssignedCreatorList, + getLiveByCreator, + getContentByCreator, + getContentDonationByCreator, + getCommunityByCreator, + getChannelDonationByCreator +} diff --git a/src/components/SideMenu.vue b/src/components/SideMenu.vue index ba7ea0a..fa2b1e1 100644 --- a/src/components/SideMenu.vue +++ b/src/components/SideMenu.vue @@ -92,8 +92,21 @@ export default { this.isLoading = true try { let res = await api.getMenus(); - if (res.status === 200 && res.data.success === true && res.data.data.length > 0) { - this.items = res.data.data + if (res.status === 200 && res.data.success === true) { + if (res.data.data && res.data.data.length > 0) { + this.items = res.data.data + } else { + // 빈 메뉴일 경우 기본 단독 메뉴를 제공한다. + // 요구사항: 정산 관련 기본 메뉴를 추가한다. + this.items = [ + { title: '소속 크리에이터', route: '/agent/creators' }, + { title: '크리에이터별 라이브 정산', route: '/agent/calculate/live' }, + { title: '크리에이터별 콘텐츠 정산', route: '/agent/calculate/content-by-date' }, + { title: '크리에이터별 콘텐츠 후원 정산', route: '/agent/calculate/content-donation-by-date' }, + { title: '크리에이터별 커뮤니티 정산', route: '/agent/calculate/community-post' }, + { title: '크리에이터별 채널 후원 정산', route: '/agent/calculate/channel-donation' }, + ] + } } else { this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!") this.logoutWithoutNetwork(); diff --git a/src/router/index.js b/src/router/index.js index b5444eb..1fa7b8e 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -20,6 +20,37 @@ const routes = [ name: 'ContentList', component: () => import(/* webpackChunkName: "content" */ '../views/Content/ContentList.vue') }, + { + path: '/agent/creators', + name: 'AgentCreators', + component: () => import(/* webpackChunkName: "agent" */ '../views/Agent/Creators.vue') + }, + // Agent-only calculate routes (placeholders) + { + path: '/agent/calculate/live', + name: 'AgentCalculateLive', + component: () => import(/* webpackChunkName: "agent-calc" */ '../views/Agent/Calculate/AgentCalculateLive.vue') + }, + { + path: '/agent/calculate/content-by-date', + name: 'AgentCalculateContent', + component: () => import(/* webpackChunkName: "agent-calc" */ '../views/Agent/Calculate/AgentCalculateContent.vue') + }, + { + path: '/agent/calculate/content-donation-by-date', + name: 'AgentCalculateContentDonation', + component: () => import(/* webpackChunkName: "agent-calc" */ '../views/Agent/Calculate/AgentCalculateContentDonation.vue') + }, + { + path: '/agent/calculate/community-post', + name: 'AgentCalculateCommunityPost', + component: () => import(/* webpackChunkName: "agent-calc" */ '../views/Agent/Calculate/AgentCalculateCommunityPost.vue') + }, + { + path: '/agent/calculate/channel-donation', + name: 'AgentCalculateChannelDonation', + component: () => import(/* webpackChunkName: "agent-calc" */ '../views/Agent/Calculate/AgentCalculateChannelDonation.vue') + }, { path: '/content/category/list', name: 'ContentCategoryList', diff --git a/src/views/Agent/Calculate/AgentCalculateChannelDonation.vue b/src/views/Agent/Calculate/AgentCalculateChannelDonation.vue new file mode 100644 index 0000000..0ecf92f --- /dev/null +++ b/src/views/Agent/Calculate/AgentCalculateChannelDonation.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/views/Agent/Calculate/AgentCalculateCommunityPost.vue b/src/views/Agent/Calculate/AgentCalculateCommunityPost.vue new file mode 100644 index 0000000..f98cff1 --- /dev/null +++ b/src/views/Agent/Calculate/AgentCalculateCommunityPost.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/views/Agent/Calculate/AgentCalculateContent.vue b/src/views/Agent/Calculate/AgentCalculateContent.vue new file mode 100644 index 0000000..83846dd --- /dev/null +++ b/src/views/Agent/Calculate/AgentCalculateContent.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/views/Agent/Calculate/AgentCalculateContentDonation.vue b/src/views/Agent/Calculate/AgentCalculateContentDonation.vue new file mode 100644 index 0000000..91c2078 --- /dev/null +++ b/src/views/Agent/Calculate/AgentCalculateContentDonation.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/views/Agent/Calculate/AgentCalculateLive.vue b/src/views/Agent/Calculate/AgentCalculateLive.vue new file mode 100644 index 0000000..65c04e8 --- /dev/null +++ b/src/views/Agent/Calculate/AgentCalculateLive.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/views/Agent/Creators.vue b/src/views/Agent/Creators.vue new file mode 100644 index 0000000..922a39b --- /dev/null +++ b/src/views/Agent/Creators.vue @@ -0,0 +1,179 @@ + + + + +