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: 10 additions & 0 deletions apps/web/src/dev/UiComponentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@/features/place';
import type { Post } from '@/features/post';
import { OriginalPostLink, PostInfo, SavedPostCard, SavedPostContext } from '@/features/post';
import { ExpandableCaption } from '@/features/post/components/ExpandableCaption';
import {
Icon16Chat,
Icon16Info,
Expand Down Expand Up @@ -964,6 +965,15 @@ export function UiComponentsPage() {
</p>
</Section>

<Section title="post — ExpandableCaption (게시물 본문)">
<div className="mx-auto w-full max-w-[343px]">
<ExpandableCaption caption={MOCK_POST.caption ?? ''} />
</div>
<p className="text-b3 text-gray-50">
한 줄로 접혀 있고 "더보기"로 펼칩니다. 펼친 뒤엔 "접기" 말고 본문을 눌러도 접힙니다.
</p>
</Section>

<Section title="post — SavedPostContext / OriginalPostLink / PostInfo">
<Row label="SavedPostContext — 장소 연결 화면 상단 안내 띠">
<div className="w-full max-w-[343px]">
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/features/post/PostDetailPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,15 @@ describe('게시물 상세', () => {
expect(screen.getByRole('button', { name: '접기' })).toBeInTheDocument();
});

it('펼친 본문을 다시 누르면 접힌다', async () => {
await renderPost(1);

fireEvent.click(screen.getByRole('button', { name: '더보기' }));
fireEvent.click(screen.getByRole('button', { name: /초록뷰가 아름다운 카페 공간/ }));

expect(screen.getByRole('button', { name: '더보기' })).toBeInTheDocument();
});

it('메모하기에서 저장하면 postId 와 새 메모로 updatePostMemo 를 호출한다', async () => {
await renderPost(1);

Expand Down
23 changes: 2 additions & 21 deletions apps/web/src/features/post/PostDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useIsAuthenticated } from '@/features/auth/session/AuthSessionProvider'
import { capturePostHogEvent } from '@/lib/posthog';
import { useBackInterceptor } from '@/shared/lib/backInterceptors';
import { useHistoryBackedFlag } from '@/shared/lib/useHistoryBackedFlag';
import { cn } from '@/shared/lib/utils';
import { useToast } from '@/shared/toast';
import { BackButton, Header } from '@/shared/ui';
import {
Expand All @@ -19,6 +18,7 @@ import {
useUpdatePlaceBookmark,
useUpdatePostMemo,
} from './api/queries';
import { ExpandableCaption } from './components/ExpandableCaption';
import { MemoSheet } from './components/MemoSheet';
import { OriginalPostLink } from './components/OriginalPostLink';
import { PlaceDirectInputDrawer } from './components/PlaceDirectInputDrawer';
Expand Down Expand Up @@ -51,7 +51,6 @@ export function PostDetailPage() {
const postDetailState = usePostDetail(postId);
const updateMemoMutation = useUpdatePostMemo(postId);
const [memoOpen, setMemoOpen] = useState(false);
const [expanded, setExpanded] = useState(false);
// 뒤로가기(버튼·하드웨어 백·스와이프)로 닫혀야 해서 히스토리 엔트리로 승격한다.
const [viewerOpen, openViewer, closeViewer] = useHistoryBackedFlag('imageViewer');
const relatedPlacesState = useRelatedPlaces(postId);
Expand Down Expand Up @@ -198,25 +197,7 @@ export function PostDetailPage() {
<div className="flex flex-col gap-2 px-4 pt-1">
<h1 className="text-h2 font-semibold text-gray-100">{title}</h1>

{post.caption ? (
<div className="flex flex-col">
<p
className={cn(
'whitespace-pre-wrap text-b2 font-normal text-gray-80',
expanded ? '' : 'line-clamp-1',
)}
>
{post.caption}
</p>
<button
type="button"
onClick={() => setExpanded((prev) => !prev)}
className="self-start text-b2 font-medium text-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-100"
>
{expanded ? '접기' : '더보기'}
</button>
</div>
) : null}
{post.caption ? <ExpandableCaption caption={post.caption} /> : null}

<PostInfo
archives={archives}
Expand Down
53 changes: 53 additions & 0 deletions apps/web/src/features/post/components/ExpandableCaption.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { ExpandableCaption } from './ExpandableCaption';

const CAPTION = '초록뷰가 아름다운 카페 공간';

describe('ExpandableCaption', () => {
it('접혀 있을 땐 본문이 버튼이 아니고 "더보기"로만 펼친다', () => {
render(<ExpandableCaption caption={CAPTION} />);

expect(screen.queryByRole('button', { name: CAPTION })).not.toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: '더보기' }));
expect(screen.getByRole('button', { name: '접기' })).toBeInTheDocument();
});

it('펼친 뒤에는 본문을 눌러도 접힌다', () => {
render(<ExpandableCaption caption={CAPTION} />);

fireEvent.click(screen.getByRole('button', { name: '더보기' }));
fireEvent.click(screen.getByRole('button', { name: CAPTION }));

expect(screen.getByRole('button', { name: '더보기' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: CAPTION })).not.toBeInTheDocument();
});

it('본문 텍스트를 고르는 중이면 눌러도 접히지 않는다', () => {
render(<ExpandableCaption caption={CAPTION} />);

fireEvent.click(screen.getByRole('button', { name: '더보기' }));
const body = screen.getByRole('button', { name: CAPTION });

const range = document.createRange();
range.selectNodeContents(body);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);

fireEvent.click(body);

expect(screen.getByRole('button', { name: '접기' })).toBeInTheDocument();
selection?.removeAllRanges();
});

it('"접기" 버튼으로도 접힌다', () => {
render(<ExpandableCaption caption={CAPTION} />);

fireEvent.click(screen.getByRole('button', { name: '더보기' }));
fireEvent.click(screen.getByRole('button', { name: '접기' }));

expect(screen.getByRole('button', { name: '더보기' })).toBeInTheDocument();
});
});
56 changes: 56 additions & 0 deletions apps/web/src/features/post/components/ExpandableCaption.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useRef, useState } from 'react';
import { cn } from '@/shared/lib/utils';

/**
* 게시물 본문 — 접혀 있을 땐 한 줄만 보이고 "더보기"로 펼친다.
*
* 펼친 뒤엔 "접기" 버튼뿐 아니라 본문을 다시 눌러도 접힌다. 긴 본문을 펼치면 접기
* 버튼이 화면 아래로 밀려나서, 읽던 자리에서 바로 접을 수 있어야 한다. 접힌 본문은
* 그대로 텍스트다 — 펼치는 건 "더보기" 뿐이다.
*
* 접는 본문은 `tabIndex={-1}` 로 탭 순서에서 뺀다 — 키보드·보조기기는 바로 아래
* "접기" 버튼으로 같은 일을 하므로 탭 정지점이 둘일 이유가 없다.
*/
export function ExpandableCaption({ caption, className }: { caption: string; className?: string }) {
const [expanded, setExpanded] = useState(false);
const bodyRef = useRef<HTMLButtonElement>(null);

/**
* 본문을 드래그하거나 길게 눌러 텍스트를 고르면 손을 뗄 때 click 이 따라온다 —
* 복사하려던 것뿐인데 접히면 안 되므로, 본문 안에 잡힌 선택이 있으면 넘긴다.
*/
function collapseUnlessSelecting() {
const selection = window.getSelection();
if (selection && !selection.isCollapsed && bodyRef.current?.contains(selection.anchorNode)) {
return;
}
setExpanded(false);
}

return (
<div className={cn('flex flex-col', className)}>
{expanded ? (
<button
ref={bodyRef}
type="button"
tabIndex={-1}
onClick={collapseUnlessSelecting}
className="select-text whitespace-pre-wrap text-left text-b2 font-normal text-gray-80"
>
{caption}
</button>
) : (
<p className="line-clamp-1 whitespace-pre-wrap text-b2 font-normal text-gray-80">
{caption}
</p>
)}
<button
type="button"
onClick={() => setExpanded((prev) => !prev)}
className="self-start text-b2 font-medium text-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-100"
>
{expanded ? '접기' : '더보기'}
</button>
</div>
);
}
23 changes: 2 additions & 21 deletions apps/web/src/features/share/SharedPostDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { ArchiveEmpty } from '@/features/archive/components/ArchiveEmpty';
import { useLoginGate } from '@/features/auth/session/useLoginGate';
import { PlaceRow } from '@/features/place';
import { toPlace } from '@/features/post/api/queries';
import { ExpandableCaption } from '@/features/post/components/ExpandableCaption';
import { OriginalPostLink } from '@/features/post/components/OriginalPostLink';
import { PostImages } from '@/features/post/components/PostImages';
import { PostImageViewer } from '@/features/post/components/PostImageViewer';
import { Icon16Archive, Icon16ArrowDown, Icon16Pen } from '@/shared/icons/NookIcons';
import { useHistoryBackedFlag } from '@/shared/lib/useHistoryBackedFlag';
import { cn } from '@/shared/lib/utils';
import { useToast } from '@/shared/toast';
import { BackButton, COLOR_BG_CLASS, EditableTextRow, Header } from '@/shared/ui';
import { useSaveSharedPost, useSharedPostDetail } from './api/queries';
Expand All @@ -31,7 +31,6 @@ export function SharedPostDetailPage() {
const { showToast } = useToast();
const { gate, wall: loginWall } = useLoginGate();
const [sheetOpen, setSheetOpen] = useState(false);
const [expanded, setExpanded] = useState(false);
// 뒤로가기(버튼·하드웨어 백·스와이프)로 닫혀야 해서 히스토리 엔트리로 승격한다.
const [viewerOpen, openViewer, closeViewer] = useHistoryBackedFlag('imageViewer');

Expand Down Expand Up @@ -96,25 +95,7 @@ export function SharedPostDetailPage() {
<div className="flex flex-col gap-2 px-4 pt-1">
<h1 className="text-h2 font-semibold text-gray-100">{title}</h1>

{post.caption ? (
<div className="flex flex-col">
<p
className={cn(
'whitespace-pre-wrap text-b2 font-normal text-gray-80',
expanded ? '' : 'line-clamp-1',
)}
>
{post.caption}
</p>
<button
type="button"
onClick={() => setExpanded((prev) => !prev)}
className="self-start text-b2 font-medium text-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-100"
>
{expanded ? '접기' : '더보기'}
</button>
</div>
) : null}
{post.caption ? <ExpandableCaption caption={post.caption} /> : null}

{/* Figma `게시물 정보 > 공유받은화면` — 아카이브 칩과 메모 줄. */}
<div className="flex min-h-6 items-center gap-2 pt-2">
Expand Down
Loading