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
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<title>THIP, 독서를 기록하는 가장 힙한 방법</title>
<meta property="og:title" content="THIP, 독서를 기록하는 가장 힙한 방법" />
<meta property="og:description" content="커뮤니티형 독서 기록 플랫폼. 띱. THIP." />
<meta property="og:image" content="https://thip.co.kr/assets/thumbnail.png" />
<meta property="og:image" content="https://thip.co.kr/assets/thumbnail.jpeg" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

OG 이미지 URL 전환(.png → .jpeg) 자체는 적절합니다만, 배포 전 헤더/용량/타입을 점검하세요.

소셜 미리보기 안정성을 위해 200 OK, Content-Type: image/jpeg, 적절한 Cache-Control, 과도하지 않은 용량(권장 < 1MB)을 확인하면 좋습니다. 또, 변경 직후 크롤러 캐시 영향이 있을 수 있으니 URL 접근성도 함께 확인 바랍니다.

다음 스크립트로 기본 헤더와 용량을 점검할 수 있습니다:

또한 최소 권장 해상도(예: 1200x630)에 부합하는지 확인이 필요하면, 아래 스크립트로 이미지 실제 해상도를 점검할 수 있습니다:

필요하시면 위 검증을 CI에 넣을 간단한 체크 스크립트도 마련해 드릴 수 있습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

URL="https://thip.co.kr/assets/thumbnail.jpeg"

echo "[HEAD 체크]"
curl -sI "$URL" | sed -n '1,20p'

echo
echo "[핵심 헤더 추출]"
curl -sI "$URL" | awk '
  BEGIN{IGNORECASE=1}
  /^HTTP\//{print "Status: "$0}
  /^content-type:/{print}
  /^content-length:/{print}
  /^cache-control:/{print}
  /^etag:/{print}
  /^last-modified:/{print}
'

echo
echo "[용량(KB) 추정]"
LEN=$(curl -sI "$URL" | awk 'BEGIN{IGNORECASE=1}/^content-length:/{print $2}' | tr -d '\r')
if [[ -n "${LEN:-}" ]]; then
  KB=$((LEN/1024))
  echo "Content-Length: ${KB} KB"
else
  echo "Content-Length 헤더 없음(전송 인코딩 chunked 가능성). 실제 파일 크기 확인 권장."
fi

Length of output: 1151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - << 'PY'
import io, sys
from urllib.request import urlopen
from PIL import Image  # pillow 필요

url = "https://thip.co.kr/assets/thumbnail.jpeg"
with urlopen(url) as resp:
    data = resp.read()
img = Image.open(io.BytesIO(data))
print(f"Format={img.format}, Size={img.size[0]}x{img.size[1]}")
w, h = img.size
if w < 1200 or h < 630:
    print("WARN: 권장 최소 해상도(1200x630) 미만일 수 있습니다.")
else:
    print("OK: 권장 해상도 충족.")
PY

Length of output: 146


OG 이미지 HTTP 응답 검증 필요: 최종 URL 헤더·타입·용량 점검

소셜 미리뷰 안정화를 위해, 아래 사항을 꼭 확인하세요.

  • 307 리다이렉트로 인해 초기 응답이 text/plain으로 돌아올 수 있음
    → curl -sI -L 로 최종 응답 헤더를 확인해야 함
  • HTTP/2 200 OK
  • Content-Type: image/jpeg
  • Content-Length 헤더 존재(권장 < 1 MB)
  • Cache-Control, ETag/Last-Modified 설정
  • 크롤러 캐시 영향 방지

아래 스크립트로 기본 검증을 수행할 수 있습니다:

#!/usr/bin/env bash
set -euo pipefail

URL="https://thip.co.kr/assets/thumbnail.jpeg"

echo "[최종 응답 헤더 확인]"
curl -sI -L "$URL" | awk '
  BEGIN{IGNORECASE=1}
  /^HTTP\//{print "Status: "$0}
  /^content-type:/{print}
  /^content-length:/{print}
  /^cache-control:/{print}
  /^etag:/{print}
  /^last-modified:/{print}
'

LEN=$(curl -sI -L "$URL" | awk 'BEGIN{IGNORECASE=1}/^content-length:/{print $2}' | tr -d '\r')
if [[ -n "$LEN" ]]; then
  KB=$((LEN/1024))
  echo "Content-Length: ${KB} KB"
else
  echo "Content-Length 헤더 없음 (chunked encoding 가능성)"
fi

필요 시 해상도(권장 1200×630 이상) 확인용 스크립트 예:

#!/usr/bin/env bash
set -euo pipefail

URL="https://thip.co.kr/assets/thumbnail.jpeg"
TMP="$(mktemp --suffix=.jpeg)"
curl -sL "$URL" -o "$TMP"

# ImageMagick identify 필요
identify -format "Format=%m, Size=%wx%h\n" "$TMP" || echo "ImageMagick 설치 필요"
rm -f "$TMP"

CI에 통합하거나, Python+Pillow 환경이 준비되어 있으면 원래 스크립트를 그대로 사용하셔도 좋습니다.

🤖 Prompt for AI Agents
index.html lines 10-10: The OG image URL may be returning intermediate redirects
or incorrect headers (e.g., text/plain, missing Content-Length, wrong
Content-Type) which breaks social previews; verify and fix by ensuring the final
URL (follow redirects) responds with HTTP/2 200, Content-Type: image/jpeg, a
Content-Length header (preferably <1MB), and proper caching headers
(Cache-Control and ETag or Last-Modified); update the server/static
hosting/configuration so the thumbnail is served directly as an image (no
307->text/plain intermediates), add or enable Content-Length, and set caching
headers; additionally validate image dimensions (>=1200x630) and re-run a final
curl -sI -L and image inspection to confirm compliance.

<meta property="og:url" content="https://thip.co.kr" />
</head>
<body>
Expand Down
Binary file added public/assets/custom_favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/thumbnail.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/assets/thumbnail.png
Binary file not shown.
4 changes: 2 additions & 2 deletions src/components/common/Post/PostBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ const PostBody = ({
</PostContent>
<ImageContainer>
{hasImage && (
<div className="imgContainer">
<>
{contentUrls.map((src: string, i: number) => (
<img key={i} src={src} />
))}
</div>
</>
)}
</ImageContainer>
</Container>
Expand Down
2 changes: 1 addition & 1 deletion src/components/feed/FollowList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getRecentFollowing, type RecentWriterData } from '@/api/users/getRecent
const FollowList = () => {
const navigate = useNavigate();
const [myFollowings, setMyFollowings] = useState<RecentWriterData[]>([]);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);

// API에서 최근 글 작성한 팔로우 리스트 조회
const fetchRecentFollowing = async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/feed/MyFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { colors, typography } from '@/styles/global/global';
import TotalBar from './TotalBar';
import { getMyProfile } from '@/api/feeds/getMyProfile';
import type { MyProfileData } from '@/types/profile';
import LoadingSpinner from '@/components/common/LoadingSpinner';

const MyFeed = ({ showHeader, posts = [], isLast = false }: FeedListProps) => {
const [profileData, setProfileData] = useState<MyProfileData | null>(null);
Expand All @@ -34,7 +33,8 @@ const MyFeed = ({ showHeader, posts = [], isLast = false }: FeedListProps) => {

if (loading || !profileData) {
return (
<LoadingSpinner message="내 피드 정보를 불러오는 중..." size="large" fullHeight={true} />
<></>
// <LoadingSpinner message="내 피드 정보를 불러오는 중..." size="large" fullHeight={true} />
);
}

Expand Down
47 changes: 33 additions & 14 deletions src/pages/feed/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import TabBar from '../../components/feed/TabBar';
import MyFeed from '../../components/feed/MyFeed';
import TotalFeed from '../../components/feed/TotalFeed';
import MainHeader from '@/components/common/MainHeader';
import LoadingSpinner from '../../components/common/LoadingSpinner';
import writefab from '../../assets/common/writefab.svg';
import { useNavigate, useLocation } from 'react-router-dom';
import { getTotalFeeds } from '@/api/feeds/getTotalFeed';
Expand Down Expand Up @@ -43,6 +44,11 @@ const Feed = () => {
const [myNextCursor, setMyNextCursor] = useState<string>('');
const [myIsLast, setMyIsLast] = useState(false);

// 탭 전환 시 로딩 상태
const [tabLoading, setTabLoading] = useState(false);
// 초기 로딩 상태 (첫 진입 시)
const [initialLoading, setInitialLoading] = useState(true);

const handleSearchButton = () => {
navigate('/feed/search');
};
Expand Down Expand Up @@ -157,10 +163,17 @@ const Feed = () => {
return;
}

if (activeTab === '피드') {
loadTotalFeeds();
} else if (activeTab === '내 피드') {
loadMyFeeds();
setTabLoading(true);

try {
if (activeTab === '피드') {
await loadTotalFeeds();
} else if (activeTab === '내 피드') {
await loadMyFeeds();
}
} finally {
setTabLoading(false);
setInitialLoading(false);
}
Comment on lines +166 to 177
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

토큰 미존재 시 초기 로딩이 영구 지속되는 버그 가능성

현재 토큰이 없으면 Line 160-164에서 조기 return 되며, 이 분기는 try/finally 블록(여기, Line 169-177) 바깥이어서 setInitialLoading(false)가 호출되지 않습니다. 결과적으로 스피너가 영구히 떠 있을 수 있습니다.

최소 수정안: 토큰 체크를 try/finally 내부로 옮기거나, 조기 return 전에 initialLoading을 false로 내려주세요.

권장 수정 예시(토큰 체크를 try/finally 안으로 이동):

-      // localStorage에 토큰이 있는지 확인
-      const authToken = localStorage.getItem('authToken');
-      if (!authToken) {
-        console.log('❌ 토큰이 없어서 피드를 로드할 수 없습니다.');
-        return;
-      }
-
-      setTabLoading(true);
-      try {
-        if (activeTab === '피드') {
-          await loadTotalFeeds();
-        } else if (activeTab === '내 피드') {
-          await loadMyFeeds();
-        }
-      } finally {
-        setTabLoading(false);
-        setInitialLoading(false);
-      }
+      setTabLoading(true);
+      try {
+        // localStorage에 토큰이 있는지 확인
+        const authToken = localStorage.getItem('authToken');
+        if (!authToken) {
+          console.log('❌ 토큰이 없어서 피드를 로드할 수 없습니다.');
+          return; // finally 보장
+        }
+        if (activeTab === '피드') {
+          await loadTotalFeeds();
+        } else if (activeTab === '내 피드') {
+          await loadMyFeeds();
+        }
+      } finally {
+        setTabLoading(false);
+        setInitialLoading(false);
+      }

대안: 조기 return 직전에 setInitialLoading(false)를 호출해도 됩니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTabLoading(true);
try {
if (activeTab === '피드') {
await loadTotalFeeds();
} else if (activeTab === '내 피드') {
await loadMyFeeds();
}
} finally {
setTabLoading(false);
setInitialLoading(false);
}
setTabLoading(true);
try {
// localStorage에 토큰이 있는지 확인
const authToken = localStorage.getItem('authToken');
if (!authToken) {
console.log('❌ 토큰이 없어서 피드를 로드할 수 없습니다.');
return; // finally 블록에서 초기 로딩 상태가 해제됩니다
}
if (activeTab === '피드') {
await loadTotalFeeds();
} else if (activeTab === '내 피드') {
await loadMyFeeds();
}
} finally {
setTabLoading(false);
setInitialLoading(false);
}
🤖 Prompt for AI Agents
In src/pages/feed/Feed.tsx around lines 166-177, the early return when no token
occurs outside the try/finally block so setInitialLoading(false) (and
setTabLoading(false)) never runs, leaving the spinner active; move the token
existence check into the try block (so the finally always clears loading flags)
or, if you keep the early return, ensure you call setInitialLoading(false) (and
optionally setTabLoading(false)) immediately before returning.

};

Expand All @@ -171,18 +184,24 @@ const Feed = () => {
<Container>
<MainHeader type="home" leftButtonClick={handleSearchButton} />
<TabBar tabs={tabs} activeTab={activeTab} onTabClick={setActiveTab} />
{activeTab === '피드' ? (
<>
<TotalFeed
showHeader={true}
posts={totalFeedPosts}
isMyFeed={false}
isLast={totalIsLast}
/>
</>
{initialLoading || tabLoading ? (
<LoadingSpinner size="large" fullHeight={true} />
) : (
<>
<MyFeed showHeader={false} posts={myFeedPosts} isMyFeed={true} isLast={myIsLast} />
{activeTab === '피드' ? (
<>
<TotalFeed
showHeader={true}
posts={totalFeedPosts}
isMyFeed={false}
isLast={totalIsLast}
/>
</>
) : (
<>
<MyFeed showHeader={false} posts={myFeedPosts} isMyFeed={true} isLast={myIsLast} />
</>
)}
</>
)}
<NavBar src={writefab} path="/post/create" />
Expand Down
27 changes: 24 additions & 3 deletions src/pages/mypage/Mypage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { useNavigate } from 'react-router-dom';
import { colors, typography } from '@/styles/global/global';
import MenuButton from '@/components/Mypage/MenuButton';
import LoadingSpinner from '@/components/common/LoadingSpinner';
import { usePopupActions } from '@/hooks/usePopupActions';
import { useLogout } from '@/hooks/useLogout';
import alert from '../../assets/mypage/alert.svg';
Expand All @@ -17,14 +18,22 @@ import { useEffect, useState } from 'react';

const Mypage = () => {
const [profile, setProfile] = useState<GetMyProfileResponse['data'] | null>(null);
const [loading, setLoading] = useState(true);
const { openConfirm, closePopup } = usePopupActions();
const navigate = useNavigate();
const { handleLogout: logout } = useLogout();

useEffect(() => {
const fetchProfile = async () => {
const profile = await getMyProfile();
setProfile(profile);
try {
setLoading(true);
const profile = await getMyProfile();
setProfile(profile);
} catch (error) {
console.error('프로필 정보 로드 실패:', error);
} finally {
setLoading(false);
}
};
fetchProfile();
}, []);
Expand All @@ -33,8 +42,20 @@ const Mypage = () => {
navigate('/mypage/edit', { state: { profile } });
};

if (loading) {
return (
<Wrapper>
<LoadingSpinner message="내 정보를 불러오는 중..." size="large" fullHeight={true} />
</Wrapper>
);
}

if (!profile) {
return <div>로딩 중...</div>;
return (
<Wrapper>
<div>프로필 정보를 불러올 수 없습니다.</div>
</Wrapper>
);
}

const handleLogout = () => {
Expand Down