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
10 changes: 9 additions & 1 deletion src/apis/courses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { apiClient } from './common';
import { apiClient, normalizeApiError } from './common';

export type CourseDetailItem =
| {
Expand Down Expand Up @@ -68,6 +68,14 @@ export async function getCourseDetail(
return data;
}

export async function deleteCourse(courseId: number): Promise<void> {
try {
await apiClient.delete(`/courses/${courseId}`);
} catch (error) {
throw normalizeApiError(error);
}
}

// 코스·문화콘텐츠 좋아요 등록은 PUT이다(장소만 POST). 여러 번 눌러도 같은
// 결과가 되도록 백엔드가 바꿨다.
export async function addCourseLike(
Expand Down
54 changes: 37 additions & 17 deletions src/components/common/CourseCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import people from '../../assets/icons/people.svg';

import { useScaleFrame } from '../../hooks/useScaleFrame';

import ReviewActionMenu from './ReviewActionMenu';
import TagChip, { type TagType } from './TagChip';

function getCompanionIcon(label?: string | null) {
Expand Down Expand Up @@ -67,8 +68,13 @@ export interface CourseCardProps {
companion: string;
tags: TagType[];
liked?: boolean;
/** 본인이 등록한 코스면 좋아요 대신 더보기(수정/삭제) 메뉴를 같은 자리에 띄운다. */
isMine?: boolean;
onClick?: () => void;
onLikeClick?: () => void;
onEditClick?: () => void;
showEdit?: boolean;
onDeleteClick?: () => void;
}

function useVisibleItemCount(itemsKey: string, itemCount: number) {
Expand Down Expand Up @@ -138,8 +144,12 @@ function CourseCard({
companion,
tags,
liked = false,
isMine = false,
onClick,
onLikeClick,
onEditClick,
showEdit,
onDeleteClick,
}: CourseCardProps) {
const { outerRef, innerRef, scale, scaledHeight } =
useScaleFrame(CARD_DESIGN_WIDTH);
Expand All @@ -148,7 +158,7 @@ function CourseCard({
{ key: 'duration', icon: calendar, label: duration },
{ key: 'courseType', icon: location, label: courseType },
{ key: 'companion', icon: people, label: companion },
];
].filter((item) => Boolean(item.label));

const metaKey = metaItems.map((item) => item.label).join('|');
const tagsKey = tags.join('|');
Expand Down Expand Up @@ -309,23 +319,33 @@ function CourseCard({
</div>
</div>

{/* Like */}
<button
type="button"
aria-pressed={liked}
onClick={(e) => {
e.stopPropagation();
onLikeClick?.();
}}
className="absolute"
style={{ top: HEART_TOP, right: HEART_RIGHT }}
>
<img
src={liked ? oheart : heart}
alt="좋아요"
style={{ height: HEART_SIZE, width: HEART_SIZE }}
{/* Like / 더보기: 본인 코스는 좋아요 대신 같은 자리에 수정·삭제 메뉴를 띄운다. */}
{isMine && (onEditClick || onDeleteClick) ? (
<ReviewActionMenu
onEditClick={onEditClick}
showEdit={showEdit}
onDeleteClick={onDeleteClick}
triggerClassName="absolute top-[12px] right-[12px]"
ariaLabel="코스 메뉴"
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</button>
) : (
<button
type="button"
aria-pressed={liked}
onClick={(e) => {
e.stopPropagation();
onLikeClick?.();
}}
className="absolute"
style={{ top: HEART_TOP, right: HEART_RIGHT }}
>
<img
src={liked ? oheart : heart}
alt="좋아요"
style={{ height: HEART_SIZE, width: HEART_SIZE }}
/>
</button>
)}
</div>
</div>
);
Expand Down
91 changes: 49 additions & 42 deletions src/components/common/PromotionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import oheart from '../../assets/icons/oheart.svg';

import { useScaleFrame } from '../../hooks/useScaleFrame';

// 모든 수치는 Figma 390 디자인 기준 리터럴 px.
// 개별 scale 계산 대신 useScaleFrame이 전체를 한 번에 scale한다.
import ReviewActionMenu from './ReviewActionMenu';

const CARD_DESIGN_WIDTH = 342;
const HEART_SIZE = 16;
const HEART_TOP = 8;
Expand All @@ -16,9 +16,6 @@ const PROFILE_GAP = 8;
const HEADER_PADDING_X = 12;
const HEADER_PADDING_TOP = 12;
const HEADER_PADDING_BOTTOM = 8;
// 프로필 헤더(아바타+이름/날짜) 블록의 실제 렌더 높이. 좋아요 버튼이
// 카드 전체 기준 좌표로 옮겨가면서, 이미지 상단이 아니라 이 높이만큼
// 아래에서부터 HEART_TOP을 더해야 원래와 같은 위치(이미지 우상단)에 온다.
const PROFILE_HEADER_HEIGHT =
HEADER_PADDING_TOP + AVATAR_SIZE + HEADER_PADDING_BOTTOM;
const NAME_SIZE = 14;
Expand Down Expand Up @@ -46,8 +43,11 @@ export interface PromotionCardProps {
title: string;
description: string;
location: string;
isMine?: boolean;
liked?: boolean;
onClick?: () => void;
onEditClick?: () => void;
onDeleteClick?: () => void;
onLikeClick?: () => void;
className?: string;
}
Expand All @@ -60,8 +60,11 @@ function PromotionCard({
title,
description,
location: locationText,
isMine = false,
liked = false,
onClick,
onEditClick,
onDeleteClick,
onLikeClick,
className = '',
}: PromotionCardProps) {
Expand All @@ -74,10 +77,6 @@ function PromotionCard({
className={`w-full overflow-hidden ${className}`}
style={{ height: scaledHeight }}
>
{/* 카드 이동(role="button")과 좋아요 버튼을 형제 컨트롤로 분리하기
위한 래퍼. transform/너비는 원래 카드 요소가 갖던 것을 그대로
옮겨왔고, useScaleFrame의 innerRef는 안쪽 카드 요소(w-full)에
둬서 offsetHeight 계산(콘텐츠 높이 그대로)에는 영향이 없다. */}
<div
className="relative"
style={{
Expand All @@ -99,51 +98,59 @@ function PromotionCard({
tabIndex={0}
className="flex w-full cursor-pointer flex-col overflow-hidden rounded-xl bg-white text-left shadow-[0_1px_5px_rgba(0,0,0,0.07)]"
>
{/* Profile header */}
<div
className="flex items-center"
className="flex items-center justify-between"
style={{
gap: PROFILE_GAP,
paddingLeft: HEADER_PADDING_X,
paddingRight: HEADER_PADDING_X,
paddingTop: HEADER_PADDING_TOP,
paddingBottom: HEADER_PADDING_BOTTOM,
}}
>
<img
src={avatarUrl}
alt=""
aria-hidden="true"
className="shrink-0 rounded-full object-cover"
style={{ width: AVATAR_SIZE, height: AVATAR_SIZE }}
/>

<div className="min-w-0">
<p
className="truncate font-semibold leading-none text-[#1C1C1C]"
style={{ fontSize: NAME_SIZE }}
>
{profileName}
</p>

<p
className="font-normal leading-none text-[#7F7F7F]"
style={{ fontSize: DATE_SIZE, marginTop: DATE_GAP }}
>
{date}
</p>
<div
className="flex min-w-0 items-center"
style={{ gap: PROFILE_GAP }}
>
<img
src={avatarUrl}
alt=""
aria-hidden="true"
className="shrink-0 rounded-full object-cover"
style={{ width: AVATAR_SIZE, height: AVATAR_SIZE }}
/>

<div className="min-w-0">
<p
className="truncate font-semibold leading-none text-[#1C1C1C]"
style={{ fontSize: NAME_SIZE }}
>
{profileName}
</p>
<p
className="font-normal leading-none text-[#7F7F7F]"
style={{ fontSize: DATE_SIZE, marginTop: DATE_GAP }}
>
{date}
</p>
</div>
</div>

{isMine && (onEditClick || onDeleteClick) ? (
<ReviewActionMenu
onEditClick={onEditClick}
onDeleteClick={onDeleteClick}
ariaLabel="홍보글 메뉴"
/>
) : null}
</div>

{/* Image */}
<img
src={imageUrl}
alt={title}
className="w-full object-cover"
style={{ height: IMAGE_HEIGHT }}
/>

{/* Body */}
<div
style={{
paddingLeft: BODY_PADDING_X,
Expand All @@ -158,14 +165,12 @@ function PromotionCard({
>
{title}
</h2>

<p
className="line-clamp-2 font-normal leading-[1.45] text-[#7F7F7F]"
style={{ fontSize: DESCRIPTION_SIZE, marginTop: DESCRIPTION_GAP }}
>
{description}
</p>

<div style={{ marginTop: LOCATION_GAP }}>
<div
className="flex items-center"
Expand All @@ -181,7 +186,6 @@ function PromotionCard({
height: LOCATION_ICON_SIZE,
}}
/>

<span
className="font-medium leading-none text-[#7F7F7F]"
style={{ fontSize: LOCATION_TEXT_SIZE }}
Expand All @@ -193,10 +197,13 @@ function PromotionCard({
</div>
</div>

{onLikeClick && (
{!isMine && onLikeClick ? (
<button
type="button"
onClick={() => onLikeClick()}
onClick={(event) => {
event.stopPropagation();
onLikeClick();
}}
aria-pressed={liked}
className="absolute"
style={{
Expand All @@ -210,7 +217,7 @@ function PromotionCard({
style={{ height: HEART_SIZE, width: HEART_SIZE }}
/>
</button>
)}
) : null}
</div>
</div>
);
Expand Down
20 changes: 14 additions & 6 deletions src/components/common/ReviewActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ export interface ReviewActionMenuProps {
* 코스 후기 전체보기)은 수정을 열지 않는다.
*/
onEditClick?: () => void;
showEdit?: boolean;
onDeleteClick?: () => void;
/** 카드마다 버튼이 놓이는 자리가 달라 트리거 배치는 밖에서 정한다. */
triggerClassName?: string;
/** 카드 종류마다 스크린리더 안내를 다르게 하려면 넘긴다. */
ariaLabel?: string;
}

/**
Expand All @@ -30,8 +33,10 @@ export interface ReviewActionMenuProps {
*/
function ReviewActionMenu({
onEditClick,
showEdit = false,
onDeleteClick,
triggerClassName = '-mt-[3px] -mr-[3px] ml-2',
ariaLabel = '리뷰 메뉴',
}: ReviewActionMenuProps) {
const scale = useGlobalScale();
const triggerId = useId();
Expand Down Expand Up @@ -95,11 +100,9 @@ function ReviewActionMenu({

updatePanelStyle();
window.addEventListener('resize', updatePanelStyle);
window.addEventListener('scroll', updatePanelStyle, true);

return () => {
window.removeEventListener('resize', updatePanelStyle);
window.removeEventListener('scroll', updatePanelStyle, true);
};
}, [isOpen, scale]);

Expand All @@ -112,9 +115,14 @@ function ReviewActionMenu({
// 붙이지만, 핸들러를 넘기지 않은 호출부에서 눌러도 아무 일 없는 항목이
// 남지 않게 한다.
const menuItems = [
{ key: 'edit', label: '수정', action: onEditClick },
{ key: 'delete', label: '삭제', action: onDeleteClick },
].filter((item) => Boolean(item.action));
{
key: 'edit',
label: '수정',
action: onEditClick,
show: showEdit || Boolean(onEditClick),
},
{ key: 'delete', label: '삭제', action: onDeleteClick, show: Boolean(onDeleteClick) },
].filter((item) => item.show);

if (menuItems.length === 0) {
return null;
Expand All @@ -126,7 +134,7 @@ function ReviewActionMenu({
id={triggerId}
ref={triggerRef}
type="button"
aria-label="리뷰 메뉴"
aria-label={ariaLabel}
aria-haspopup="menu"
aria-expanded={isOpen}
aria-controls={menuId}
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/AuthSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const LOGOUT_GAP = 12;
const MY_MENU: { label: string; path?: string }[] = [
{ label: '여행기록', path: '/travel-record' },
{ label: '좋아요', path: '/likes' },
{ label: '내가 등록한 게시물' },
{ label: '내가 등록한 게시물', path: '/my-posts' },
];

interface AuthSidebarProps {
Expand Down
15 changes: 15 additions & 0 deletions src/constants/courseFilterLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ const LIKED_ITEM_FILTER_COLUMN_CLASS_NAMES: Record<string, string> = {
export const getLikedItemFilterColumnClassName = (filterKey: string) =>
LIKED_ITEM_FILTER_COLUMN_CLASS_NAMES[filterKey] ?? 'col-start-1';

/**
* 내가 등록한 게시물 필터는 칩이 2개(분류/정렬)뿐이라, 좋아요 필터와 같은
* auto + 1fr 스페이서로 정렬 칩만 오른쪽 끝에 붙인다.
*/
export const MY_POST_FILTER_GRID_CLASS_NAME =
'grid w-[342px] origin-top-left grid-cols-[auto_1fr_auto] items-center gap-2 max-[389px]:[transform:scale(calc((100vw_-_48px)/342px))]';

const MY_POST_FILTER_COLUMN_CLASS_NAMES: Record<string, string> = {
category: 'col-start-1',
sort: 'col-start-3 justify-self-end',
};

export const getMyPostFilterColumnClassName = (filterKey: string) =>
MY_POST_FILTER_COLUMN_CLASS_NAMES[filterKey] ?? 'col-start-1';

const COURSE_FILTER_COLUMN_CLASS_NAMES: Record<string, string> = {
transport: 'col-start-1',
duration: 'col-start-3',
Expand Down
Loading