Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9650cc7
feat: 피드 작성 기능 구현
ljh130334 Aug 12, 2025
528fb8f
fix: BookSearchBottomSheet 더미 데이터에 ISBN 정보 추가
ljh130334 Aug 12, 2025
7674110
feat: 새 글 작성 API 연동 완료 및 파일 업로드 최적화
ljh130334 Aug 12, 2025
c7e3e9c
fix: 이미지 필드명 불일치 수정 (contentsUrl → contentUrls)
ljh130334 Aug 13, 2025
9fc7737
fix: SearchBook 컴포넌트 타입 오류 해결
ljh130334 Aug 13, 2025
28642dd
fix: console.log 삭제
ljh130334 Aug 13, 2025
007a16c
Merge pull request #102 from THIP-TextHip/feat/api-feeds-create
ljh130334 Aug 13, 2025
858e17d
Merge branch 'develop' of https://github.com/THIP-TextHip/THIP-Web in…
heeeeyong Aug 13, 2025
412a91f
design: BookInfoCard width 수정(반응형 이슈)
heeeeyong Aug 13, 2025
94c0a87
feat: 피드 수정 기능 구현
ljh130334 Aug 13, 2025
f22057d
fix: 무한 렌더링 문제 해결
ljh130334 Aug 13, 2025
0bf6ec7
fix: 빈 이미지 src 문제 해결
ljh130334 Aug 13, 2025
fcf18ea
fix: 피드 수정 API 요청 형식을 JSON으로 변경
ljh130334 Aug 13, 2025
f1c5f03
fix: console.log 및 주석 제거
ljh130334 Aug 13, 2025
54e4045
fix: 사용자 검색 isFinalized response 변수 추가
heeeeyong Aug 13, 2025
654d9c4
feat: 내 띱 리스트 API 연동
heeeeyong Aug 13, 2025
e2b5426
Merge pull request #103 from THIP-TextHip/feat/api-feeds-modify
ljh130334 Aug 13, 2025
52cc5dc
feat: 소셜로그인 쿠키 방식 구현
heeeeyong Aug 13, 2025
ecd21f1
Merge pull request #104 from THIP-TextHip/feat/api-auth
heeeeyong Aug 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import Router from './pages';
import { Global } from '@emotion/react';
import { CookiesProvider } from 'react-cookie';
import { globalStyles } from './styles/global/global';
import PopupContainer from './components/common/Modal/PopupContainer';

const App = () => {
return (
<CookiesProvider>
<>
<Global styles={globalStyles} />
<Router />
<PopupContainer />
</CookiesProvider>
</>
);
};

Expand Down
55 changes: 55 additions & 0 deletions src/api/feeds/createFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { apiClient } from '../index';

/** 서버에 보낼 request JSON 페이로드 */
export interface CreateFeedBody {
isbn: string;
contentBody: string;
isPublic: boolean;
tagList?: string[];
}

/** 성공 응답 */
export interface CreateFeedSuccess {
isSuccess: true;
code: number;
message: string;
data: {
feedId: number;
};
}

/** 실패 응답 */
export interface CreateFeedFail {
isSuccess: false;
code: number;
message: string;
}

export type CreateFeedResponse = CreateFeedSuccess | CreateFeedFail;

/**
* 피드 작성 API
* - multipart/form-data
* - request: application/json (Blob로 감싸 전송)
* - images: File[] (선택값, 없으면 미첨부)
*/
export const createFeed = async (
body: CreateFeedBody,
images?: File[],
): Promise<CreateFeedResponse> => {
const form = new FormData();

// request 파트(JSON) - 필수
form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' }));

// images 파트들 - 선택
if (images && images.length > 0) {
images.forEach(file => form.append('images', file));
}

const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, {
headers: { 'Content-Type': 'multipart/form-data' },
});

return data;
};
8 changes: 5 additions & 3 deletions src/api/feeds/getFeedDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,25 @@ export interface FeedDetailData {
creatorId: number;
creatorNickname: string;
creatorProfileImageUrl: string;
aliasName: string;
alias: string;
aliasColor: string;
postDate: string;
isbn: string;
bookTitle: string;
bookAuthor: string;
contentBody: string;
contentsUrl: string[];
contentUrls: string[];
likeCount: number;
commentCount: number;
isSaved: boolean;
isLiked: boolean;
isPublic: boolean;
tagList: string[];
}

// API 응답 타입
export interface FeedDetailResponse {
success: boolean;
isSuccess: boolean;
code: number;
message: string;
data: FeedDetailData;
Expand All @@ -40,4 +41,5 @@ export const getFeedDetail = async (feedId: number) => {
const feedDetail = await getFeedDetail(123);
console.log(feedDetail.data.feedId); // 123
console.log(feedDetail.data.tagList); // ["태그1", "태그2"]
console.log(feedDetail.data.isPublic); // true or false
*/
2 changes: 1 addition & 1 deletion src/api/feeds/getMyFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface MyFeedData {

// API 응답 타입
export interface MyFeedResponse {
success: boolean;
isSuccess: boolean;
code: number;
message: string;
data: MyFeedData;
Expand Down
4 changes: 2 additions & 2 deletions src/api/feeds/getOtherFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface OtherFeedItem {
bookTitle: string;
bookAuthor: string;
contentBody: string;
contentsUrl: string[];
contentUrls: string[];
likeCount: number;
commentCount: number;
isSaved: boolean;
Expand All @@ -21,7 +21,7 @@ export interface OtherFeedData {

// API 응답 타입
export interface OtherFeedResponse {
success: boolean;
isSuccess: boolean;
code: number;
message: string;
data: OtherFeedData;
Expand Down
2 changes: 1 addition & 1 deletion src/api/feeds/getTotalFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface TotalFeedData {

// API 응답 타입
export interface TotalFeedResponse {
success: boolean;
isSuccess: boolean;
code: number;
message: string;
data: TotalFeedData;
Expand Down
88 changes: 88 additions & 0 deletions src/api/feeds/updateFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { apiClient } from '../index';

/** 피드 수정 요청 바디 타입 */
export interface UpdateFeedBody {
contentBody: string;
isPublic: boolean;
tagList?: string[];
remainImageUrls?: string[];
}

/** 성공 응답 */
export interface UpdateFeedSuccess {
isSuccess: true;
code: number;
message: string;
}

/** 실패 응답 */
export interface UpdateFeedFail {
isSuccess: false;
code: number;
message: string;
}

export type UpdateFeedResponse = UpdateFeedSuccess | UpdateFeedFail;

/**
* 피드 수정 API
* - multipart/form-data
* - request: application/json (Blob로 감싸 전송)
* - 이미지 추가는 불가능, 기존 이미지 삭제만 가능
*/
export const updateFeed = async (
feedId: number,
body: UpdateFeedBody,
): Promise<UpdateFeedResponse> => {
try {
const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, body, {
headers: { 'Content-Type': 'application/json' },
});

return data;
} catch (error) {
console.error('수정 API 에러:', error);

const form = new FormData();
form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' }));

const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, {
headers: { 'Content-Type': 'multipart/form-data' },
});

return data;
}
};

/*
사용 예시:

// 기존 이미지 일부 유지 (새 이미지 추가는 불가)
const updateBody: UpdateFeedBody = {
contentBody: "수정된 글 내용입니다!",
isPublic: true,
tagList: ["한국소설", "책추천", "역사"],
remainImageUrls: ["https://img.domain.com/1.jpg"] // 기존 이미지 중 유지할 것들
};

try {
const result = await updateFeed(123, updateBody);
if (result.isSuccess) {
console.log('피드 수정 성공:', result.message);
} else {
console.error('피드 수정 실패:', result.message);
}
} catch (error) {
console.error('네트워크 오류:', error);
}

// 모든 이미지 삭제 후 텍스트만 수정
const textOnlyUpdate: UpdateFeedBody = {
contentBody: "텍스트만 수정",
isPublic: false,
tagList: [],
remainImageUrls: [] // 모든 기존 이미지 삭제
};

const result = await updateFeed(123, textOnlyUpdate);
*/
128 changes: 128 additions & 0 deletions src/api/images/uploadImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { apiClient } from '../index';

/** 단일 이미지 업로드 성공 시 데이터 */
export interface UploadImageData {
imageUrl: string;
}

/** 서버 공통 응답 타입 */
export interface UploadImageResponse {
isSuccess: boolean;
code: number;
message: string;
data?: UploadImageData; // 성공 시에만 존재
}

/** 내부 유틸: 허용 확장자 */
const IMAGE_EXT_REGEX = /\.(jpe?g|png|gif)$/i;
/** 가이드 최대 업로드 개수 (서버는 FEED 생성 시 최대 3장 제약) */
export const MAX_IMAGES = 3;

/** 파일 사전 검증: 빈 파일 / 확장자 */
function validateFile(file: File) {
if (!file || file.size === 0) {
// 서버 코드 170001과 의미 일치
throw new Error('업로드하려는 이미지가 비어있습니다.');
}
if (!IMAGE_EXT_REGEX.test(file.name)) {
// 서버 코드 170003과 의미 일치
throw new Error('파일 형식은 jpg, jpeg, png, gif만 가능합니다.');
}
}

/** 단일 이미지 업로드 */
export const uploadImage = async (
file: File,
options?: { signal?: AbortSignal },
): Promise<UploadImageResponse> => {
// 사전 검증
validateFile(file);

const formData = new FormData();
// 서버가 단일 업로드에서 기대하는 필드명이 image라면 유지
formData.append('image', file);

const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
signal: options?.signal,
});

return data;
};

/**
* 다중 이미지 업로드
* - 전부 성공하면 URL 배열 반환
* - 하나라도 실패하면 실패 내역을 포함해 throw
*/
export const uploadMultipleImages = async (
files: File[],
options?: { signal?: AbortSignal; enforceMax?: boolean },
): Promise<string[]> => {
// 개수 제한(선택) – FEED 생성 정책에 맞춰 사전 차단하고 싶을 때 사용
if (options?.enforceMax && files.length > MAX_IMAGES) {
throw new Error(`이미지는 최대 ${MAX_IMAGES}장까지 업로드할 수 있습니다.`);
}

// 파일별 사전 검증
files.forEach(validateFile);

// 병렬 업로드 (각 요청 독립)
const results = await Promise.allSettled(
files.map(file => uploadImage(file, { signal: options?.signal })),
);

const successUrls: string[] = [];
const failures: { index: number; reason: string }[] = [];

results.forEach((res, idx) => {
if (res.status === 'fulfilled') {
const value = res.value;
if (value.isSuccess && value.data?.imageUrl) {
successUrls.push(value.data.imageUrl);
} else {
failures.push({
index: idx,
reason: value.message || '파일 업로드에 실패하였습니다.',
});
}
} else {
failures.push({
index: idx,
reason: (res.reason as Error)?.message || '네트워크 오류로 파일 업로드에 실패하였습니다.',
});
}
});

if (failures.length > 0) {
// 어떤 항목이 왜 실패했는지 상세 메시지
const detail = failures.map(f => `#${f.index + 1}: ${f.reason}`).join(' / ');
throw new Error(`일부 이미지 업로드에 실패했습니다. (${detail})`);
}

return successUrls;
};

/*
사용 예시:

// 단일
try {
const res = await uploadImage(file);
if (res.isSuccess) {
console.log('업로드된 URL:', res.data?.imageUrl);
} else {
console.error('실패:', res.message);
}
} catch (e) {
console.error('오류:', e);
}

// 다중
try {
const urls = await uploadMultipleImages(files, { enforceMax: true });
console.log('업로드된 URL들:', urls);
} catch (e) {
console.error('다중 업로드 실패:', e);
}
*/
Loading