diff --git a/api.md b/api.md index e07a559..9d66c00 100644 --- a/api.md +++ b/api.md @@ -139,7 +139,7 @@ "tags": [ "Match" ], - "summary": "크리에이터 매칭 분석", + "summary": "크리에이터 매칭 분석 by 정윤철", "description": "크리에이터 정보를 기반으로 매칭 분석 결과와 추천 브랜드 목록을 반환합니다.\nuserType, typeTag, highMatchingBrandList를 포함합니다.\n", "operationId": "match", "requestBody": { @@ -568,12 +568,144 @@ } } }, + "/api/v1/brands": { + "get": { + "tags": [ + "Brand" + ], + "summary": "브랜드 전체 목록 조회 (페이징) by 이예림", + "description": "등록된 모든 브랜드의 리스트를 페이징하여 반환합니다.", + "operationId": "getAllBrands", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Zero-based page index (0..N)", + "required": false, + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + } + }, + { + "name": "size", + "in": "query", + "description": "The size of the page to be returned", + "required": false, + "schema": { + "type": "integer", + "default": 10, + "minimum": 1 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CustomResponsePageBrandListResponseDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Brand" + ], + "summary": "브랜드 생성 by 이예림", + "description": "새로운 브랜드를 등록합니다.", + "operationId": "createBrand", + "requestBody": { + "description": "생성할 브랜드 정보", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrandCreateRequestDto" + }, + "example": { + "brandName": "비플레인", + "industryType": "BEAUTY", + "logoUrl": "https://cdn.your-service.com/brands/beplain/logo.png", + "simpleIntro": "천연 유래 성분으로 민감 피부를 위한 저자극 스킨케어 브랜드", + "detailIntro": "티끌없는 순수 히알루론산™으로 피부속부터 촉촉한 #수분세럼\n민감성 피부도 부담 없이 사용할 수 있는 저자극·천연재료 기반의 뷰티 브랜드입니다.", + "homepageUrl": "https://www.beplain.co.kr", + "brandCategory": [ + "스킨케어", + "메이크업" + ], + "brandSkinCareTag": { + "skinType": [ + "건성", + "지성", + "복합성" + ], + "mainFunction": [ + "수분/보습", + "진정" + ] + }, + "brandMakeUpTag": { + "skinType": [ + "건성", + "민감성" + ], + "brandMakeUpStyle": [ + "내추럴", + "글로우" + ] + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "생성 성공", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CustomResponseBrandCreateResponseDto" + } + } + } + }, + "400": { + "description": "잘못된 요청", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CustomResponse" + } + } + } + } + } + } + }, "/api/v1/brands/{brandId}/like": { "post": { "tags": [ "Brand" ], - "summary": "브랜드 좋아요 토글", + "summary": "브랜드 좋아요 토글 by 이예림", "description": "브랜드 ID로 좋아요를 추가하거나 취소합니다.", "operationId": "likeBrand", "parameters": [ @@ -888,6 +1020,164 @@ } } }, + "/api/v1/campaigns/proposal/{campaignProposalId}/approve": { + "patch": { + "tags": [ + "Business" + ], + "summary": "받은 캠페인 제안 수락 API by 박지영", + "description": "제안 받은 사람이 제안을 수락하는 API입니다.\n/api/v1/campaigns/proposal/{campaignProposalId}에서 status가 MATCH로 변경되었다면 성공입니다.\n", + "operationId": "approveCampaignProposal", + "parameters": [ + { + "name": "campaignProposalId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CustomResponseString" + } + } + } + } + } + } + }, + "/api/v1/brands/{brandId}": { + "get": { + "tags": [ + "Brand" + ], + "summary": "브랜드 상세 조회 by 이예림", + "description": "브랜드 ID로 상세 정보를 조회합니다.", + "operationId": "getBrandDetail", + "parameters": [ + { + "name": "brandId", + "in": "path", + "description": "조회할 브랜드의 ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "조회 성공", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CustomResponseListBrandDetailResponseDto" + } + } + } + }, + "404": { + "description": "존재하지 않는 브랜드", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CustomResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Brand" + ], + "summary": "브랜드 삭제 by 이예림", + "description": "브랜드 ID로 브랜드를 삭제합니다. (소프트 삭제)", + "operationId": "deleteBrand", + "parameters": [ + { + "name": "brandId", + "in": "path", + "description": "삭제할 브랜드의 ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "삭제 성공" + }, + "404": { + "description": "존재하지 않는 브랜드", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CustomResponse" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Brand" + ], + "summary": "브랜드 정보 수정 by 이예림", + "description": "특정 브랜드의 정보를 수정합니다.", + "operationId": "updateBrand", + "parameters": [ + { + "name": "brandId", + "in": "path", + "description": "수정할 브랜드의 ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "description": "수정할 브랜드 정보", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrandUpdateRequestDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "수정 성공" + }, + "404": { + "description": "존재하지 않는 브랜드", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CustomResponse" + } + } + } + } + } + } + }, "/api/v1/users/me": { "get": { "tags": [ @@ -1004,7 +1294,7 @@ "tags": [ "Tag" ], - "summary": "태그 타입별 조회", + "summary": "태그 타입별 조회 by 정윤철", "description": "지정된 태그 타입의 목록을 카테고리별로 조회합니다.", "operationId": "getTagsByType", "parameters": [ @@ -1036,7 +1326,7 @@ "tags": [ "Tag" ], - "summary": "패션 태그 조회", + "summary": "패션 태그 조회 by 정윤철", "description": "패션 태그 목록을 카테고리별로 조회합니다.", "operationId": "getFashionTags", "responses": { @@ -1058,7 +1348,7 @@ "tags": [ "Tag" ], - "summary": "컨텐츠 태그 조회", + "summary": "컨텐츠 태그 조회 by 정윤철", "description": "컨텐츠 태그 목록을 조회합니다.", "operationId": "getContentTags", "responses": { @@ -1080,7 +1370,7 @@ "tags": [ "Tag" ], - "summary": "뷰티 태그 조회", + "summary": "뷰티 태그 조회 by 정윤철", "description": "뷰티 태그 목록을 카테고리별로 조회합니다.", "operationId": "getBeautyTags", "responses": { @@ -1102,7 +1392,7 @@ "tags": [ "Match" ], - "summary": "매칭 캠페인 목록 조회 및 검색", + "summary": "매칭 캠페인 목록 조회 및 검색 by 정윤철", "description": "JWT 토큰의 사용자 ID를 기반으로 매칭 캠페인 목록을 검색하거나 매칭 캠페인 목록을 조회합니다.\n\n**검색**: keyword를 입력하면 캠페인명(title)만 검색합니다. (브랜드명, 설명 등은 검색 대상 제외)\n\n**정렬 옵션**:\n- MATCH_SCORE: 매칭률 순 (동점 시 인기순 우선)\n- POPULARITY: 인기 순 (좋아요 수)\n- REWARD_AMOUNT: 금액 순 (원고료 높은 순)\n- D_DAY: 마감 순 (마감 임박순)\n\n**카테고리 필터**: ALL(전체), FASHION(패션), BEAUTY(뷰티)\n\n**페이지네이션**: page(0부터 시작), size(기본 20)\n", "operationId": "getMatchingCampaigns", "parameters": [ @@ -1200,7 +1490,7 @@ "tags": [ "Match" ], - "summary": "매칭 브랜드 목록 조회", + "summary": "매칭 브랜드 목록 조회 by 정윤철", "description": "JWT 토큰의 사용자 ID를 기반으로 매칭률이 높은 브랜드 목록을 조회합니다.\n정렬 옵션: MATCH_SCORE(매칭률 순), POPULARITY(인기순), NEWEST(신규순)\n카테고리 필터: ALL(전체), FASHION(패션), BEAUTY(뷰티)\n태그 필터: 뷰티/패션 관련 태그로 필터링\n", "operationId": "getMatchingBrands", "parameters": [ @@ -1605,56 +1895,12 @@ } } }, - "/api/v1/brands/{brandId}": { - "get": { - "tags": [ - "Brand" - ], - "summary": "브랜드 상세 조회", - "description": "브랜드 ID로 상세 정보를 조회합니다.", - "operationId": "getBrandDetail", - "parameters": [ - { - "name": "brandId", - "in": "path", - "description": "조회할 브랜드의 ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "조회 성공", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/CustomResponseListBrandDetailResponseDto" - } - } - } - }, - "404": { - "description": "존재하지 않는 브랜드", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/CustomResponse" - } - } - } - } - } - } - }, "/api/v1/brands/{brandId}/sponsor-products": { "get": { "tags": [ "Brand" ], - "summary": "브랜드 협찬 가능 제품 리스트 조회", + "summary": "브랜드 협찬 가능 제품 리스트 조회 by 이예림", "description": "특정 브랜드의 협찬 가능 제품 목록을 조회합니다.", "operationId": "getSponsorProducts", "parameters": [ @@ -1688,7 +1934,7 @@ "tags": [ "Brand" ], - "summary": "협찬 가능 제품 상세 조회", + "summary": "협찬 가능 제품 상세 조회 by 이예림", "description": "브랜드의 특정 협찬 가능 제품 상세 정보를 조회합니다.", "operationId": "getSponsorProductDetail", "parameters": [ @@ -1871,12 +2117,57 @@ } } }, + "/api/v1/brands/user/{userId}": { + "get": { + "tags": [ + "Brand" + ], + "summary": "유저 ID로 브랜드 ID 조회 by 이예림", + "description": "유저 ID에 해당하는 브랜드 ID를 조회합니다.", + "operationId": "getBrandIdByUserId", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "조회할 유저의 ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "조회 성공", + "content": { + "*/*": { + "schema": { + "type": "integer", + "format": "int64" + } + } + } + }, + "404": { + "description": "존재하지 않는 유저 또는 브랜드", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CustomResponse" + } + } + } + } + } + } + }, "/api/v1/brands/filters": { "get": { "tags": [ "Brand" ], - "summary": "브랜드 필터 옵션 조회", + "summary": "브랜드 필터 옵션 조회 by 이예림", "description": "브랜드 필터링에 사용될 옵션들을 조회합니다.", "operationId": "getBrandFilters", "responses": { @@ -2278,9 +2569,15 @@ "MatchResponseDto": { "type": "object", "properties": { + "username": { + "type": "string" + }, "userType": { "type": "string" }, + "userTypeImage": { + "type": "string" + }, "typeTag": { "type": "array", "items": { @@ -2478,7 +2775,126 @@ "usageRanges" ] }, - "CustomResponse": { + "BrandClothingTagDto": { + "type": "object", + "properties": { + "brandType": { + "type": "array", + "items": { + "type": "string" + } + }, + "brandStyle": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "BrandCreateRequestDto": { + "type": "object", + "properties": { + "brandName": { + "type": "string" + }, + "industryType": { + "type": "string", + "enum": [ + "BEAUTY", + "FASHION" + ] + }, + "logoUrl": { + "type": "string" + }, + "simpleIntro": { + "type": "string" + }, + "detailIntro": { + "type": "string" + }, + "homepageUrl": { + "type": "string" + }, + "brandCategory": { + "type": "array", + "items": { + "type": "string" + } + }, + "brandSkinCareTag": { + "$ref": "#/components/schemas/BrandSkinCareTagDto" + }, + "brandMakeUpTag": { + "$ref": "#/components/schemas/BrandMakeUpTagDto" + }, + "brandClothingTag": { + "$ref": "#/components/schemas/BrandClothingTagDto" + } + } + }, + "BrandMakeUpTagDto": { + "type": "object", + "properties": { + "skinType": { + "type": "array", + "items": { + "type": "string" + } + }, + "brandMakeUpStyle": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "BrandSkinCareTagDto": { + "type": "object", + "properties": { + "skinType": { + "type": "array", + "items": { + "type": "string" + } + }, + "mainFunction": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CustomResponse": { + "type": "object", + "properties": { + "isSuccess": { + "type": "boolean" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "result": { + + } + } + }, + "BrandCreateResponseDto": { + "type": "object", + "properties": { + "brandId": { + "type": "integer", + "format": "int64" + } + } + }, + "CustomResponseBrandCreateResponseDto": { "type": "object", "properties": { "isSuccess": { @@ -2491,7 +2907,7 @@ "type": "string" }, "result": { - + "$ref": "#/components/schemas/BrandCreateResponseDto" } } }, @@ -2723,7 +3139,10 @@ } }, "skinBrightness": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "makeupStyle": { "type": "array", @@ -2748,6 +3167,9 @@ "ContentsTypeUpdate": { "type": "object", "properties": { + "snsUrl": { + "type": "string" + }, "viewerGender": { "type": "array", "items": { @@ -2795,7 +3217,7 @@ "FashionTypeUpdate": { "type": "object", "properties": { - "bodyStats": { + "height": { "type": "string" }, "bodyShape": { @@ -2841,6 +3263,41 @@ } } }, + "BrandUpdateRequestDto": { + "type": "object", + "properties": { + "brandName": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "simpleIntro": { + "type": "string" + }, + "detailIntro": { + "type": "string" + }, + "homepageUrl": { + "type": "string" + }, + "brandCategory": { + "type": "array", + "items": { + "type": "string" + } + }, + "brandSkinCareTag": { + "$ref": "#/components/schemas/BrandSkinCareTagDto" + }, + "brandMakeUpTag": { + "$ref": "#/components/schemas/BrandMakeUpTagDto" + }, + "brandClothingTag": { + "$ref": "#/components/schemas/BrandClothingTagDto" + } + } + }, "CustomResponseMyPageResponseDto": { "type": "object", "properties": { @@ -3180,6 +3637,9 @@ "MyFeatureResponseDto": { "type": "object", "properties": { + "creatorType": { + "type": "string" + }, "beautyType": { "$ref": "#/components/schemas/BeautyType" }, @@ -3280,6 +3740,30 @@ "ContentTagResponse": { "type": "object", "properties": { + "viewerGenders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagItemResponse" + } + }, + "viewerAges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagItemResponse" + } + }, + "avgVideoLengths": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagItemResponse" + } + }, + "avgVideoViews": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagItemResponse" + } + }, "formats": { "type": "array", "items": { @@ -3536,13 +4020,28 @@ "type": "integer", "format": "int64" }, - "campaignImageUrl": { + "campaignTitle": { "type": "string" }, - "brandName": { + "sponsorProducts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CampaignSummarySponsorProductResponse" + } + } + } + }, + "CampaignSummarySponsorProductResponse": { + "type": "object", + "properties": { + "productId": { + "type": "integer", + "format": "int64" + }, + "productName": { "type": "string" }, - "campaignTitle": { + "thumbnailImageUrl": { "type": "string" } } @@ -3828,6 +4327,41 @@ } } }, + "CampaignContentTagResponse": { + "type": "object", + "properties": { + "formats": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagItemResponse" + } + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagItemResponse" + } + }, + "tones": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagItemResponse" + } + }, + "involvements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagItemResponse" + } + }, + "usageRanges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagItemResponse" + } + } + } + }, "CampaignProposalDetailResponse": { "type": "object", "properties": { @@ -3876,7 +4410,7 @@ "format": "date-time" }, "contentTags": { - "$ref": "#/components/schemas/ContentTagResponse" + "$ref": "#/components/schemas/CampaignContentTagResponse" } } }, @@ -3964,158 +4498,164 @@ } } }, - "AvailableSponsorProdDto": { + "BrandListResponseDto": { "type": "object", "properties": { - "productId": { + "brandId": { "type": "integer", "format": "int64" }, - "productName": { + "brandName": { "type": "string" }, - "availableType": { + "logoUrl": { "type": "string" }, - "availableQuantity": { - "type": "integer", - "format": "int32" - }, - "availableSize": { - "type": "integer", - "format": "int32" + "industryType": { + "type": "string" } } }, - "BrandDetailResponseDto": { + "CustomResponsePageBrandListResponseDto": { "type": "object", "properties": { - "brandName": { - "type": "string" + "isSuccess": { + "type": "boolean" }, - "brandTag": { - "type": "array", - "items": { - "type": "string" - } + "code": { + "type": "string" }, - "brandDescription": { + "message": { "type": "string" }, - "brandMatchingRatio": { + "result": { + "$ref": "#/components/schemas/PageBrandListResponseDto" + } + } + }, + "PageBrandListResponseDto": { + "type": "object", + "properties": { + "totalPages": { "type": "integer", "format": "int32" }, - "brandIsLiked": { - "type": "boolean" + "totalElements": { + "type": "integer", + "format": "int64" }, - "brandCategory": { - "type": "array", - "items": { - "type": "string" - } + "pageable": { + "$ref": "#/components/schemas/PageableObject" }, - "brandSkinCareTag": { - "$ref": "#/components/schemas/BrandSkinCareTagDto" + "numberOfElements": { + "type": "integer", + "format": "int32" }, - "brandMakeUpTag": { - "$ref": "#/components/schemas/BrandMakeUpTagDto" + "size": { + "type": "integer", + "format": "int32" }, - "brandOnGoingCampaign": { + "content": { "type": "array", "items": { - "$ref": "#/components/schemas/BrandOnGoingCampaignDto" + "$ref": "#/components/schemas/BrandListResponseDto" } }, - "availableSponsorProd": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AvailableSponsorProdDto" - } + "number": { + "type": "integer", + "format": "int32" }, - "campaignHistory": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CampaignHistoryDto" - } - } - } - }, - "BrandMakeUpTagDto": { - "type": "object", - "properties": { - "brandSkinType": { - "type": "array", - "items": { - "type": "string" - } + "sort": { + "$ref": "#/components/schemas/SortObject" }, - "brandMakeUpStyle": { - "type": "array", - "items": { - "type": "string" - } + "first": { + "type": "boolean" + }, + "last": { + "type": "boolean" + }, + "empty": { + "type": "boolean" } } }, - "BrandOnGoingCampaignDto": { + "PageableObject": { "type": "object", "properties": { - "brandId": { + "pageSize": { "type": "integer", - "format": "int64" + "format": "int32" }, - "brandName": { - "type": "string" + "paged": { + "type": "boolean" }, - "recruitingTotalNumber": { - "type": "integer", - "format": "int32" + "unpaged": { + "type": "boolean" }, - "recruitedNumber": { + "pageNumber": { "type": "integer", "format": "int32" }, - "campaginDescription": { - "type": "string" + "offset": { + "type": "integer", + "format": "int64" }, - "campaginManuscriptFee": { - "type": "string" + "sort": { + "$ref": "#/components/schemas/SortObject" } } }, - "BrandSkinCareTagDto": { + "SortObject": { "type": "object", "properties": { - "brandSkinType": { - "type": "array", - "items": { - "type": "string" - } + "unsorted": { + "type": "boolean" }, - "brandMainFunction": { - "type": "array", - "items": { - "type": "string" - } + "sorted": { + "type": "boolean" + }, + "empty": { + "type": "boolean" } } }, - "CampaignHistoryDto": { + "BrandDetailResponseDto": { "type": "object", "properties": { - "campaignId": { + "userId": { "type": "integer", "format": "int64" }, - "campaignTitle": { + "brandName": { "type": "string" }, - "startDate": { - "type": "string" + "brandTag": { + "type": "array", + "items": { + "type": "string" + } }, - "endDate": { + "brandDescription": { "type": "string" + }, + "brandMatchingRatio": { + "type": "integer", + "format": "int32" + }, + "brandIsLiked": { + "type": "boolean" + }, + "brandCategory": { + "type": "array", + "items": { + "type": "string" + } + }, + "brandSkinCareTag": { + "$ref": "#/components/schemas/BrandSkinCareTagDto" + }, + "brandMakeUpTag": { + "$ref": "#/components/schemas/BrandMakeUpTagDto" } } }, diff --git a/app/api/axios.ts b/app/api/axios.ts index 37cc1c7..7d77ebb 100644 --- a/app/api/axios.ts +++ b/app/api/axios.ts @@ -6,11 +6,9 @@ import type { } from "axios"; import { tokenStorage } from "../lib/token"; -const BASE_URL = import.meta.env.VITE_API_BASE_URL; - // Axios 인스턴스 생성 export const axiosInstance: AxiosInstance = axios.create({ - baseURL: BASE_URL, + baseURL: "/api", headers: { "Content-Type": "application/json", }, @@ -73,7 +71,7 @@ axiosInstance.interceptors.response.use( // Refresh Token으로 새 Access Token 발급 const response = await axios.post( - `${BASE_URL}/api/v1/auth/refresh`, + `/api/v1/auth/refresh`, {}, { headers: { diff --git a/app/routes/auth/api/auth.ts b/app/routes/auth/api/auth.ts index 6edcb83..a7333a0 100644 --- a/app/routes/auth/api/auth.ts +++ b/app/routes/auth/api/auth.ts @@ -1,8 +1,5 @@ -import axios from "axios"; import { apiClient } from "../../../api/axios"; import { tokenStorage } from "../../../lib/token"; - -const BASE_URL = import.meta.env.VITE_API_BASE_URL; import type { SignupCompleteRequest, SignupCompleteResponse, @@ -40,8 +37,8 @@ export const refreshToken = async (): Promise => { throw new Error("No refresh token available"); } - const response = await axios.post( - `${BASE_URL}/api/v1/auth/refresh`, + const response = await apiClient.post( + "/api/v1/auth/refresh", {}, { headers: { diff --git a/app/routes/auth/login/components/SocialLoginSection.tsx b/app/routes/auth/login/components/SocialLoginSection.tsx index df4d474..1c1962f 100644 --- a/app/routes/auth/login/components/SocialLoginSection.tsx +++ b/app/routes/auth/login/components/SocialLoginSection.tsx @@ -15,17 +15,17 @@ const BASE_URL = import.meta.env.VITE_API_BASE_URL; export function SocialLoginSection({ lastProvider }: SocialLoginSectionProps) { const handleKakaoLogin = () => { - const redirectUri = `${window.location.origin}/auth/callback/kakao`; + const redirectUri = import.meta.env.VITE_KAKAO_REDIRECT_URI || `${window.location.origin}/auth/callback/kakao`; window.location.href = `${BASE_URL}/oauth2/authorization/kakao?redirect_uri=${encodeURIComponent(redirectUri)}`; }; const handleNaverLogin = () => { - const redirectUri = `${window.location.origin}/auth/callback/naver`; + const redirectUri = import.meta.env.VITE_NAVER_REDIRECT_URI || `${window.location.origin}/auth/callback/naver`; window.location.href = `${BASE_URL}/oauth2/authorization/naver?redirect_uri=${encodeURIComponent(redirectUri)}`; }; const handleGoogleLogin = () => { - const redirectUri = `${window.location.origin}/auth/callback/google`; + const redirectUri = import.meta.env.VITE_GOOGLE_REDIRECT_URI || `${window.location.origin}/auth/callback/google`; window.location.href = `${BASE_URL}/oauth2/authorization/google?redirect_uri=${encodeURIComponent(redirectUri)}`; }; diff --git a/app/routes/business/campaign/$campaignId.tsx b/app/routes/business/campaign/$campaignId.tsx index cc63eaf..77301cb 100644 --- a/app/routes/business/campaign/$campaignId.tsx +++ b/app/routes/business/campaign/$campaignId.tsx @@ -1,5 +1,6 @@ import { useParams, useNavigate } from "react-router"; import { useState, useEffect } from "react"; +import Button from "../../../components/common/Button"; import CampaignBrandCard from "../components/CampaignBrandCard"; import CampaignInfoGroup from "../components/CampaignInfoGroup"; @@ -159,7 +160,18 @@ export default function CampaignContent() { - + +
+ +
+ ); } diff --git a/app/routes/business/proposal/api/proposal.ts b/app/routes/business/proposal/api/proposal.ts index 052dcec..7cca7bf 100644 --- a/app/routes/business/proposal/api/proposal.ts +++ b/app/routes/business/proposal/api/proposal.ts @@ -1,5 +1,4 @@ -import axios from "axios"; -import { tokenStorage } from "../../../../lib/token"; +import { axiosInstance } from "../../../../api/axios"; import type { BrandDetail } from "../../../../data/brand"; // 1. 응답 데이터의 공통 포맷 정의 (isSuccess 등을 포함) @@ -32,53 +31,28 @@ export interface ProposalDetail { } export const getProposalDetail = async (proposalId: string): Promise => { - const BASE_URL = "https://api.realmatch.co.kr"; - // 1. tokenStorage 유틸을 사용하여 안전하게 토큰을 가져옵니다. - const token = tokenStorage.getAccessToken(); - - console.log("현재 보관된 토큰:", token); - console.log("토큰 만료 여부:", tokenStorage.isTokenExpired()); - console.log("현재 사용자 ID:", tokenStorage.getUserId()); - console.log("현재 사용자 역할(Role):", tokenStorage.getRole()); - try { - const response = await axios.get(`${BASE_URL}/api/v1/campaigns/proposal/${proposalId}`, { - headers: { - // 2. 토큰이 있을 때만 Authorization 헤더를 추가합니다. - ...(token && { Authorization: `Bearer ${token}` }), - "accept": "*/*" - } - }); + const response = await axiosInstance.get>( + `/api/v1/campaigns/proposal/${proposalId}` + ); if (response.data.isSuccess) { return response.data.result; } - throw new Error(response.data.message || "데이터 로드 실패"); - } catch (error) { // : any 삭제 - if (axios.isAxiosError(error)) { // axios 에러인지 확인하는 가드 추가 (권장) - if (error.response?.status === 401) { - console.error("401 에러: 토큰이 유효하지 않거나 로그인이 필요합니다."); - } - - } + } catch (error) { + console.error("제안 상세 조회 실패:", error); throw error; } }; // 브랜드 상세 정보를 가져오는 API 함수 예시 export const getBrandDetail = async (brandId: number | string): Promise => { - const BASE_URL = "https://api.realmatch.co.kr"; - const token = tokenStorage.getAccessToken(); - try { - const response = await axios.get(`${BASE_URL}/api/v1/brands/${brandId}`, { - headers: { - ...(token && { Authorization: `Bearer ${token}` }), - "accept": "*/*" - } - }); + const response = await axiosInstance.get>( + `/api/v1/brands/${brandId}` + ); if (response.data.isSuccess) { // 스웨거 응답 구조상 result가 배열이므로 첫 번째 요소를 반환 @@ -87,7 +61,6 @@ export const getBrandDetail = async (brandId: number | string): Promise +
+ {/* Simple spinner or just text */} +
+ 로딩중... +
+
+ ); } return hasMatch ? : ; diff --git a/app/routes/matching/api/matching.ts b/app/routes/matching/api/matching.ts index 46d8c31..43e7471 100644 --- a/app/routes/matching/api/matching.ts +++ b/app/routes/matching/api/matching.ts @@ -94,6 +94,7 @@ export interface MatchingBrand { isLiked: boolean; category: string; tags?: string[]; + isRecruiting?: boolean; } interface MatchingCampaignResponse { @@ -135,11 +136,12 @@ interface MatchCampaignRawItem { interface MatchBrandRawItem { brandId: number; brandName: string; - logoUrl: string; - matchingRatio: number; + brandLogoUrl: string; + brandMatchingRatio: number; brandIsLiked?: boolean; + brandIsRecruiting?: boolean; category?: string; - tags?: string[]; + brandTags?: string[]; } // 브랜드 필터 타입 @@ -332,12 +334,13 @@ export const getMatchingBrands = async ( const brands = (response.data.result.brands || []).map((item) => ({ id: item.brandId, name: item.brandName, - logoUrl: item.logoUrl, - matchRate: item.matchingRatio || 0, - matchingRatio: item.matchingRatio, + logoUrl: item.brandLogoUrl, + matchRate: item.brandMatchingRatio || 0, + matchingRatio: item.brandMatchingRatio, isLiked: item.brandIsLiked || false, category: item.category || category, - tags: item.tags || [], + tags: item.brandTags || [], + isRecruiting: item.brandIsRecruiting || false })); return { @@ -467,8 +470,8 @@ export const createCampaignProposal = async ( ): Promise => { try { const response = await axiosInstance.post( - "/api/v1/campaigns/proposal", - data, + "/api/v1/campaign/request", + data ); if (!response.data.isSuccess) { diff --git a/app/routes/matching/campaign/campaign-content.tsx b/app/routes/matching/campaign/campaign-content.tsx index 378486b..bb1204e 100644 --- a/app/routes/matching/campaign/campaign-content.tsx +++ b/app/routes/matching/campaign/campaign-content.tsx @@ -189,6 +189,7 @@ export default function CampaignContent() { applicants={campaign.applicants} isLiked={campaign.isLiked} onLike={() => toggleLike(campaign.id)} + onClick={() => navigate(`/business/campaign/${campaign.id}`)} logoUrl={campaign.logoUrl || `/dummy-logo-${campaign.id}.png`} /> ))} diff --git a/app/routes/matching/campaign/components/CampaignCard.tsx b/app/routes/matching/campaign/components/CampaignCard.tsx index da43fc3..fa337ba 100644 --- a/app/routes/matching/campaign/components/CampaignCard.tsx +++ b/app/routes/matching/campaign/components/CampaignCard.tsx @@ -9,6 +9,7 @@ interface CampaignCardProps { isLiked?: boolean; logoUrl?: string; onLike?: () => void; + onClick?: () => void; } export default function CampaignCard({ @@ -19,7 +20,8 @@ export default function CampaignCard({ applicants, isLiked = false, logoUrl, - onLike + onLike, + onClick }: CampaignCardProps) { // 금액 포맷팅 const formatReward = (amount: number) => { @@ -27,7 +29,7 @@ export default function CampaignCard({ }; return ( -
+
{/* 왼쪽: 이미지 + 배지 */}
diff --git a/app/routes/matching/suggest/components/SuggestHeader.tsx b/app/routes/matching/suggest/components/SuggestHeader.tsx index 2b6379b..140fca7 100644 --- a/app/routes/matching/suggest/components/SuggestHeader.tsx +++ b/app/routes/matching/suggest/components/SuggestHeader.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router"; +import { useNavigate, useLocation } from "react-router"; interface SuggestHeaderProps { title: string; @@ -7,12 +7,15 @@ interface SuggestHeaderProps { export default function SuggestHeader({ title, onBack }: SuggestHeaderProps) { const navigate = useNavigate(); + const location = useLocation(); const handleBack = () => { if (onBack) { onBack(); + } else if (location.pathname.includes("/create")) { + navigate("/matching/suggest"); } else { - navigate({ to: "/" }); + navigate("/"); } }; diff --git a/app/routes/matching/suggest/create/create-campaign-content.tsx b/app/routes/matching/suggest/create/create-campaign-content.tsx index 5ff5ab9..a6c042c 100644 --- a/app/routes/matching/suggest/create/create-campaign-content.tsx +++ b/app/routes/matching/suggest/create/create-campaign-content.tsx @@ -1,5 +1,5 @@ -import { useState } from "react"; -import { useNavigate, useSearchParams } from "react-router"; +import { useState, useEffect } from "react"; +import { useNavigate, useSearchParams, useLocation } from "react-router"; import { useForm, useWatch } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { toast } from "sonner"; @@ -73,6 +73,30 @@ export default function CreateCampaignContent() { defaultValues: defaultCampaignFormValues, }); + const location = useLocation(); + + useEffect(() => { + if (type === "existing" && location.state?.campaign) { + const campaign = location.state.campaign; + + // 캠페인 데이터 매핑 + setValue("campaignName", campaign.title || ""); + setValue("description", campaign.description || ""); + + // 태그 매핑 (단일 선택으로 가정하거나 첫 번째 항목 사용) + if (campaign.contentTags?.formats?.length > 0) setValue("format", campaign.contentTags.formats[0].name); // name 또는 id 매핑 필요 (현재는 name으로 가정) + if (campaign.contentTags?.categories?.length > 0) setValue("category", campaign.contentTags.categories[0].name); + if (campaign.contentTags?.tones?.length > 0) setValue("tone", campaign.contentTags.tones[0].name); + if (campaign.contentTags?.involvements?.length > 0) setValue("involvement", campaign.contentTags.involvements[0].name); + if (campaign.contentTags?.usageRanges?.length > 0) setValue("usageScope", campaign.contentTags.usageRanges[0].name); + + setValue("fee", campaign.rewardAmount?.toString() || ""); + setValue("sponsorProduct", campaign.productId?.toString() || ""); // productId로 매핑 + setValue("startDate", campaign.startDate || ""); + setValue("endDate", campaign.endDate || ""); + } + }, [type, location.state, setValue]); + const formValues = useWatch({ control, defaultValue: defaultCampaignFormValues }); const handleToggleCampaign = (id: number) => { @@ -159,7 +183,10 @@ export default function CreateCampaignContent() { {/* 스크롤 영역 */}
{ + console.log(errors); + toast.error("모두 입력해주세요"); + })} > {/* 제목 */}

{title}

@@ -308,7 +335,7 @@ export default function CreateCampaignContent() { {/* 하단 버튼 */}
-
diff --git a/vercel.json b/vercel.json index 6aa0fb2..19a4d7f 100644 --- a/vercel.json +++ b/vercel.json @@ -3,6 +3,10 @@ "outputDirectory": "build/client", "framework": null, "rewrites": [ + { + "source": "/api/:path*", + "destination": "https://api.realmatch.co.kr/:path*" + }, { "source": "/(.*)", "destination": "/index.html" } ] }