Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 48 additions & 0 deletions src/api/feeds/getFeedsByIsbn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { apiClient } from '@/api/index';

export type FeedSort = 'like' | 'latest';

export interface FeedItem {
feedId: number;
creatorId: number;
isWriter: boolean;
creatorNickname: string;
creatorProfileImageUrl: string;
aliasName: string;
aliasColor: string;
postDate: string;
isbn: string;
bookTitle: string;
bookAuthor: string;
contentBody: string;
contentUrls: string[];
likeCount: number;
commentCount: number;
isSaved: boolean;
isLiked: boolean;
}

export interface GetFeedsByIsbnResponse {
isSuccess: boolean;
code: number;
message: string;
data: {
feeds: FeedItem[];
nextCursor: string | null;
isLast: boolean;
};
}

export const getFeedsByIsbn = async (
isbn: string,
sort: FeedSort = 'like',
cursor?: string | null,
): Promise<GetFeedsByIsbnResponse> => {
const params = new URLSearchParams();
params.set('sort', sort);
if (cursor != null) params.set('cursor', cursor);

const url = `/feeds/related-books/${encodeURIComponent(isbn)}?` + params.toString();
const res = await apiClient.get<GetFeedsByIsbnResponse>(url);
return res.data;
};
12 changes: 12 additions & 0 deletions src/assets/books/lockedBook.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 3 additions & 5 deletions src/components/group/GroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,10 @@ export const GroupCard = forwardRef<HTMLDivElement, Props>(
</Participant>
{isOngoing === true ? (
<RecruitingDeadline isRecommend={isRecommend}>
{group.deadLine}일 뒤 모집 마감
{group.deadLine} 마감
</RecruitingDeadline>
) : (
<OngoingDeadline isRecommend={isRecommend}>
{group.deadLine}일 뒤 종료
</OngoingDeadline>
<OngoingDeadline isRecommend={isRecommend}>{group.deadLine} 종료</OngoingDeadline>
)}
</Bottom>
</Info>
Expand All @@ -48,7 +46,7 @@ const Card = styled.div<{ cardType: 'main' | 'search' | 'modal' }>`
cardType === 'search' ? colors.black.main : colors.darkgrey.main};
border-top: ${({ cardType }) =>
cardType === 'search' ? `1px solid ${colors.darkgrey.dark}` : 'none'};
border: ${({ cardType }) => (cardType === 'main' ? `1px solid ${colors.grey[300]}` : 'none')};
border: ${({ cardType }) => (cardType === 'main' ? `1px solid ${colors.grey[300]}` : '')};
border-radius: ${({ cardType }) => (cardType === 'search' ? `none` : '12px')};
box-sizing: border-box;
padding: ${({ cardType }) => (cardType === 'search' ? '24px 12px 12px 12px' : '12px')};
Expand Down
95 changes: 27 additions & 68 deletions src/components/group/PasswordModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,61 @@ import styled from '@emotion/styled';
import { colors, typography } from '@/styles/global/global';
import leftArrow from '@/assets/common/leftArrow.svg';
import TitleHeader from '@/components/common/TitleHeader';
import { useNavigate } from 'react-router-dom';
import { postPassword } from '@/api/rooms/postPassword';
import { postJoinRoom } from '@/api/rooms/postJoinRoom';
import { usePopupActions } from '@/hooks/usePopupActions';

interface PasswordModalProps {
roomId: number;
onClose: () => void;
onJoined?: (roomId: number) => void; // ✅ 추가: 성공 시 부모에서 처리
}

const PasswordModal = ({ roomId }: PasswordModalProps) => {
const PasswordModal = ({ roomId, onClose, onJoined }: PasswordModalProps) => {
const [password, setPassword] = useState<string[]>(['', '', '', '']);
const [activeIndex, setActiveIndex] = useState<number>(0);
const [errorMessage, setErrorMessage] = useState<string>('');
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const navigate = useNavigate();
const { openSnackbar } = usePopupActions();

// 컴포넌트 마운트 시 상태 초기화
useEffect(() => {
setPassword(['', '', '', '']);
setActiveIndex(0);
setErrorMessage('');
}, []);

// 입력 필드 포커스 관리
useEffect(() => {
if (inputRefs.current[activeIndex]) {
inputRefs.current[activeIndex]?.focus();
}
inputRefs.current[activeIndex]?.focus();
}, [activeIndex]);

useEffect(() => {
if (errorMessage) {
const timer = setTimeout(() => {
setErrorMessage('');
}, 1500);

return () => clearTimeout(timer);
const t = setTimeout(() => setErrorMessage(''), 1500);
return () => clearTimeout(t);
}
}, [errorMessage]);

const handleClickBack = () => {
navigate(-1);
onClose(); // ✅ 라우터 대신 닫기
};

// 비밀번호 확인 API 호출
const handlePasswordConfirm = async (fullPassword: string) => {
setErrorMessage('');

try {
// 1. 비밀번호 확인 API 호출
const passwordResponse = await postPassword(roomId, { password: fullPassword });

if (passwordResponse.isSuccess && passwordResponse.data.matched) {
// 2. 비밀번호가 맞으면 방 참가 API 호출
const joinResponse = await postJoinRoom(roomId, 'join');

if (joinResponse.isSuccess) {
// 성공 후 처리 (방 상세 페이지로 이동)
navigate(`/group/detail/${roomId}`);
// 성공 snackbar 표시
openSnackbar({
message: '모임방 참여가 완료되었어요! 모집 마감 후 활동이 시작돼요.',
variant: 'top',
onClose: () => {},
});
onClose();
onJoined?.(roomId); // ✅ 부모에서 후처리(예: 상세 이동/리스트 갱신)
} else {
// 실패 후 처리 (방 상세 페이지로 이동)
navigate(`/group/detail/${roomId}`);
// 실패 snackbar 표시
openSnackbar({
message: '모임방 참여에 실패했어요. 다시 시도해 주세요.',
variant: 'top',
Expand All @@ -81,60 +66,44 @@ const PasswordModal = ({ roomId }: PasswordModalProps) => {
}
} else {
setErrorMessage('비밀번호가 일치하지 않습니다.');
// 비밀번호 입력 필드 초기화
setPassword(['', '', '', '']);
setActiveIndex(0);
}
} catch {
setErrorMessage('오류가 발생했습니다. 다시 시도해주세요.');
// 비밀번호 입력 필드 초기화
setPassword(['', '', '', '']);
setActiveIndex(0);
}
};

const handleInputChange = (index: number, value: string) => {
// 숫자만 입력 허용
if (!/^\d*$/.test(value)) return;
const next = [...password];
next[index] = value;
setPassword(next);

const newPassword = [...password];
newPassword[index] = value;
setPassword(newPassword);

// 다음 입력 필드로 이동
if (value && index < 3) {
setActiveIndex(index + 1);
}

// 4자리 모두 입력 완료 시 자동으로 비밀번호 확인
if (newPassword.every(digit => digit !== '') && index === 3) {
const fullPassword = newPassword.join('');
handlePasswordConfirm(fullPassword);
if (value && index < 3) setActiveIndex(index + 1);
if (next.every(digit => digit !== '') && index === 3) {
handlePasswordConfirm(next.join(''));
}
};

const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Backspace') {
if (password[index] === '') {
// 현재 필드가 비어있으면 이전 필드로 이동
if (index > 0) {
setActiveIndex(index - 1);
const newPassword = [...password];
newPassword[index - 1] = '';
setPassword(newPassword);
}
if (password[index] === '' && index > 0) {
setActiveIndex(index - 1);
const next = [...password];
next[index - 1] = '';
setPassword(next);
} else {
// 현재 필드 내용 삭제
const newPassword = [...password];
newPassword[index] = '';
setPassword(newPassword);
const next = [...password];
next[index] = '';
setPassword(next);
}
}
};

const handleInputClick = (index: number) => {
setActiveIndex(index);
};
const handleInputClick = (index: number) => setActiveIndex(index);

return (
<Overlay>
Expand All @@ -147,9 +116,7 @@ const PasswordModal = ({ roomId }: PasswordModalProps) => {
{password.map((digit, index) => (
<PasswordInput
key={index}
ref={(el: HTMLInputElement | null) => {
inputRefs.current[index] = el;
}}
ref={el => void (inputRefs.current[index] = el)}
type="text"
value={digit}
onChange={e => handleInputChange(index, e.target.value)}
Expand Down Expand Up @@ -184,40 +151,32 @@ const Overlay = styled.div`
align-items: center;
z-index: 2000;
`;

const Title = styled.div`
color: ${colors.white};
font-size: ${typography.fontSize.lg};
font-weight: ${typography.fontWeight.semibold};
line-height: 24px;
letter-spacing: 0.018px;
text-align: center;
margin-top: 217px;
text-align: center;
`;

const PasswordInputContainer = styled.div`
display: flex;
gap: 12px;
margin-top: 32px;
`;

const PasswordInput = styled.input<{ hasError: boolean }>`
width: 44px;
height: 44px;
border-radius: 12px;
background-color: ${colors.darkgrey.main};
border: ${props => (props.hasError ? `1px solid ${colors.red}` : 'none')};
border: ${p => (p.hasError ? `1px solid ${colors.red}` : 'none')};
outline: none;

text-align: center;
color: ${colors.white};
caret-color: ${colors.neongreen};
font-family: ${typography.fontFamily.secondary};
font-size: ${typography.fontSize.lg};
font-weight: ${typography.fontWeight.semibold};
line-height: normal;
`;

const ErrorMessage = styled.div`
color: ${colors.red};
font-size: ${typography.fontSize.sm};
Expand Down
2 changes: 1 addition & 1 deletion src/components/search/GroupSearchResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const GroupSearchResult = ({
}
onClick={() => onClickRoom(Number(group.id))}
>
<GroupCard group={group} isOngoing={group.isOnGoing} type="search" />
<GroupCard group={group} isOngoing={true} type="search" />
</div>
))
)}
Expand Down
10 changes: 9 additions & 1 deletion src/pages/groupDetail/GroupDetail.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ export const Wrapper = styled.div`
position: relative;
flex-direction: column;
align-items: center;
justify-content: center;
justify-content: flex-start;
min-width: 320px;
max-width: 767px;
min-height: 100vh;
height: 100%;
margin: 0 auto;
background-color: ${colors.black.main};
Expand Down Expand Up @@ -184,6 +185,13 @@ export const BookIntro = styled.div`
margin-top: 4px;
color: ${colors.grey[200]};
font-size: ${typography.fontSize['2xs']};

display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
`;

Expand Down
Loading