Skip to content

[FEATURE] 공지사항 API 연동#48

Merged
kbh0218 merged 4 commits into
devfrom
feat/#47-notice-api
May 23, 2026
Merged

[FEATURE] 공지사항 API 연동#48
kbh0218 merged 4 commits into
devfrom
feat/#47-notice-api

Conversation

@kbh0218

@kbh0218 kbh0218 commented May 22, 2026

Copy link
Copy Markdown
Contributor

Closes #47

개요

설정 화면의 공지사항 목록에서 기존 mock 공지 데이터를 제거하고, 실제 백엔드 Notice API 기반 조회 흐름으로 교체했습니다.

기존 app/(tabs)/(home)/notices.tsx는 하드코딩된 PLACEHOLDER_NOTICES를 표시하고 있었지만, 이번 작업에서는 GET /api/v1/notices 응답의 items, nextCursor, hasNext 구조를 화면 상태에 반영하도록 변경했습니다.

또한 공지 항목 선택 시 GET /api/v1/notices/{id}를 호출하는 상세 화면을 추가해 공지 제목, 본문, 고정 여부, 작성일, 수정일을 표시하도록 구현했습니다.

Notice API는 백엔드 dev 기준 Clerk 인증 토큰이 필요한 보호 API이므로, issue #41에서 정리한 공통 API 클라이언트 패턴을 따라 authenticatedApiRequest 기반으로 호출합니다.

주요 구현 내용

  • 공지사항 API 전용 파일 api/notices.ts 추가
  • GET /api/v1/notices 목록 조회 API 연동
  • GET /api/v1/notices/{id} 상세 조회 API 연동
  • Clerk getToken() 기반 보호 API 호출 적용
  • 공지 목록 응답의 items, nextCursor, hasNext 상태 반영
  • 공지 목록 pagination 및 더 불러오기 처리
  • 공지 목록 pull-to-refresh 처리
  • 공지 목록 로딩, 빈 목록, API 에러 상태 처리
  • 공지 상세 화면 notice-detail.tsx 추가
  • 공지 상세 로딩 및 에러 상태 처리
  • 공지 작성일/수정일 날짜 표시 형식 정리
  • 요청 중 화면 이탈 시 AbortController로 request 정리
  • 일시적 API 실패에 대한 Notice API retry 처리

파일별 역할

  • api/notices.ts: 공지 목록/상세 API 타입 및 호출 함수 추가, 인증 API 요청 및 retry 처리
  • app/(tabs)/(home)/notices.tsx: mock 목록 제거, 실제 공지 목록 조회, pagination, refresh, 로딩/빈 목록/에러 상태 처리
  • app/(tabs)/(home)/notice-detail.tsx: 공지 상세 조회 화면 추가, 상세 API 호출 및 상태별 UI 처리

해결한 이슈 목록

  • 현재 app/(tabs)/(home)/notices.tsx의 mock 공지 목록 표시 구조 확인
  • Notice API 타입과 호출 함수를 별도 API 파일로 정리
  • GET /api/v1/notices 요청 시 Clerk getToken() 기반 인증 헤더 포함
  • 공지 목록 응답의 items, nextCursor, hasNext 구조를 화면 상태에 반영
  • 공지 항목의 id, title, isPinned, createdAt 값을 화면에 표시
  • 공지 목록 로딩/빈 목록/API 에러 상태 처리
  • 공지 항목 선택 시 GET /api/v1/notices/{id} API 호출
  • 공지 상세 응답의 title, content, isPinned, createdAt, updatedAt 값을 화면에 표시
  • 공지 상세 로딩/에러 상태 처리
  • issue #41에서 만든 공통 API 클라이언트 패턴 준수

체크 사항

  • 커밋/코딩 컨벤션에 맞게 작성
  • 프론트 API 클라이언트 사용 규칙 준수
  • Notice API 파일에서 직접 fetch() 호출 없음
  • Notice API 파일에서 직접 API base URL 생성 없음
  • Notice API 파일에서 직접 Authorization 헤더 주입 없음
  • Notice API 파일에서 { data: ... } 응답 직접 unwrap 없음
  • Clerk 세션 토큰이 필요한 API는 authenticatedApiRequest 사용
  • 보호 API 호출이 /auth/me 수동 사전 호출에 의존하지 않음

참고

이번 PR은 공지사항 목록/상세 조회 API 연동만 포함합니다.

백엔드 dev 기준 Notice API가 인증 필요 API이므로, 공지 목록과 상세 조회 모두 Clerk 세션 토큰 기반 보호 API 호출로 구현했습니다.

Screenshots or Video

  • 공지사항 눌렀을 때 뜨는 화면입니다
image
  • 공지사항 상세조회 시 뜨는 화면입니다
image

@kbh0218 kbh0218 requested review from minsoo0506 and sunm2n May 22, 2026 06:48
@kbh0218 kbh0218 self-assigned this May 22, 2026
@kbh0218 kbh0218 added the feature 기능개발 label May 22, 2026
const [isRefreshing, setIsRefreshing] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const getTokenRef = useRef(getToken);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ref 2개 추가 필요

isLoadingMoreRef 필요한 이유

handleLoadMore는 isLoadingMore state 값으로 중복 진입을 막는다. 그런데 React의 상태 업데이트는 비동기 입니다.
loadNotices가 호출되면 그 안에서 setIsLoadingMore(true)를 호출하지만,
이 값이 실제로 컴포넌트에 반영(re-render)되는 건 현재 JS 실행이 끝난 다음 렌더 사이클 입니다.

Android FlatList는 스크롤 이벤트 처리 중 onEndReached를 짧은 시간 안에 두 번 이상 발화하는 것으로 알고 있습니다.
두 번 모두 re-render 전에 발화하면 isLoadingMore가 아직 false이므로
두 호출 모두 가드를 통과 → 같은 cursor로 두 번 요청 → 동일한 항목이 목록에 중복 추가되는 것으로 보입니다.

useRef는 상태가 아니라 메모리 직접 참조이므로 re-render를 기다리지 않고 즉시 값이 반영됩니다.
따라서 첫 번째 호출에서 isLoadingMoreRef.current = true로 설정하면 두 번째 호출이 같은
틱에 발생해도 즉시 차단된다.

activeRequestRef 필요한 이유

handleRefresh, handleLoadMore, 재시도 버튼은 각각 AbortController를 만들어 요청을 보내야 합니다.
그런데 이전 요청이 아직 진행 중일 때 새 요청이 시작되면 두 요청이 동시에 실행되어 나중에 완료된 응답이 먼저 완료된 응
을 덮어쓸 수 있습니다 (last-writer-wins).
activeRequestRef에 현재 진행 중인 컨트롤러를 보관하면, 새 요청 시작 전에 activeRequestRef.current?.abort()로 이전 요청을 취소할 수 있습니다.

 const getTokenRef = useRef(getToken);
// ↓ 아래에 추가
const activeRequestRef = useRef<AbortController | null>(null);
const isLoadingMoreRef = useRef(false);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

피드백 감사합니다!

말씀해주신 것처럼 isLoadingMore state만으로는 React re-render 이전에 연속으로 들어오는 onEndReached 호출을 즉시 막기 어렵다고 판단했습니다. 그래서 isLoadingMoreRef를 추가해 더 불러오기 요청 진입 시점에 동기적으로 잠그도록 반영했습니다.

또한 activeRequestRef를 추가해서 초기 로드, 새로고침, 더 불러오기, 재시도 요청이 모두 새 요청 시작 전에 기존 요청을 abort하도록 정리했습니다. 이를 통해 지연 완료된 이전 응답이 최신 목록 상태를 덮어쓰는 상황도 방지하도록 수정했습니다.

추가로 finally에서 isLoadingMoreRef.current를 초기화해 성공/실패/abort 이후 다음 페이지 요청이 막히지 않도록 처리했습니다.

}

setErrorMessage(error instanceof Error ? error.message : '공지사항을 불러오지 못했습니다.');
} finally {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

finally 블록에 ref 초기화 추가 (L81–L89)

원인

isLoadingMoreRef.current = true로 설정한 후 요청이 완료(성공 or 실패)되면 반드시 false로 초기화해야 다음 페이지 로딩이 가능합니다.
초기화를 try 블록에서 하면 에러 발생 시 실행되지 않고, catch 블록에서 하면 성공 시 누락됩니다.
finally는 성공·실패·abort 어떤 경우에도 실행이 보장되므로 여기서 초기화하는 것이 올바른 위치라고 생각됩니다.

L81–L89 기존 코드:

  } finally {
    if (signal?.aborted) {
      return;
    }
    setIsInitialLoading(false);
    setIsRefreshing(false);
    setIsLoadingMore(false);
  }

수정 제한:

  } finally {
    isLoadingMoreRef.current = false;  // ← 추가: signal 상태 무관하게 항상 초기화
    if (signal?.aborted) {
      return;
    }
    setIsInitialLoading(false);
    setIsRefreshing(false);
    setIsLoadingMore(false);
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

피드백 감사합니다!

이 부분은 앞서 isLoadingMoreRef를 추가하면서 함께 반영했습니다. isLoadingMoreRef.current가 abort 이후에도 true로 남으면 다음 페이지 로딩이 계속 막힐 수 있어서, finally 블록에서 signal?.aborted 체크보다 먼저 false로 초기화하도록 처리했습니다.

return () => controller.abort();
}, [loadNotices]);

const handleRefresh = useCallback(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

handleRefresh에 AbortSignal 추가 (L102–L104)

원인

현재 코드는 loadNotices({ refresh: true })로 signal 없이 요청을 보낸다. 이 상태에서 두 가지 문제가 생긴다.

문제 1 — 빠른 연속 당겨서 새로고침

사용자가 새로고침을 빠르게 두 번 실행하면 첫 번째 요청이 완료되지 않은 상태에서 두 번째 요청이 시작됩니다.
두 요청 모두 signal이 없어 취소가 불가능합니다.
두 번째가 먼저 완료되어 최신 목록을 보여준 뒤, 첫 번째 요청이 나중에 완료되면서 setNotices(page.items)로 더 오래된 데이터가 덮어써지는 문제가 발생합니다.

문제 2 — Expo Router 탭 네비게이션

_layout.tsx를 보면 탭은 로 구성되어 있고, React Navigation의 탭은 기본적으로 화면을 마운트 상태로 유지합니다. 사용자가 새로고침을 시작하고 다른 탭으로 이동했다가 돌아오면, 그 사이에 useEffect가 새 로드를 완료했을 수 있습니다.
이때 signal 없는 새로고침 응답이 늦게 도착하면 최신 데이터를 덮어씁니다.

해결 방법

activeRequestRef를 활용해 새 요청 전에 이전 요청을 abort하면 두 문제 모두 해결됩니다.

L102–L104 기존 코드:

  const handleRefresh = useCallback(() => {
    void loadNotices({ refresh: true });
  }, [loadNotices]);

수정 제안:

  const handleRefresh = useCallback(() => {
    activeRequestRef.current?.abort();        // 이전 요청 취소
    const controller = new AbortController();
    activeRequestRef.current = controller;
    void loadNotices({ refresh: true, signal: controller.signal });
  }, [loadNotices]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

피드백 감사합니다!

해당 내용은 앞선 요청 중복 방지 작업에 포함해서 반영해두었습니다. 현재 handleRefresh는 직접 loadNotices({ refresh: true })를 호출하지 않고, startLoadNotices({ refresh: true })를 통해 실행됩니다.

startLoadNotices 내부에서 기존 요청을 activeRequestRef.current?.abort()로 취소한 뒤 새 AbortController를 생성하고, 해당 signalloadNotices에 전달하도록 처리했습니다. 그래서 빠른 연속 새로고침이나 이전 요청의 지연 응답이 최신 목록을 덮어쓰는 상황을 방지할 수 있습니다.

void loadNotices({ refresh: true });
}, [loadNotices]);

const handleLoadMore = useCallback(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

handleLoadMore에 ref 가드 + AbortSignal 추가 (L106–L112)

원인 1 — 중복 요청 (수정 1과 연결)

isLoadingMore state 대신 isLoadingMoreRef.current로 가드를 교체합니다.
이유는 수정 1에서 설명한 것과 동일합니다.
state 기반 가드는 React re-render 이전까지 업데이트되지 않아 같은 틱에 두 번 호출되면 두 번 모두 통과합니다.

원인 2 — 취소 불가 요청

페이지 로딩 중 사용자가 새로고침을 하거나 다른 탭으로 이동하면 진행 중인 페이지 요청을 취소해야 합니다.
signal이 없으면 취소가 불가능하여 지연 도착한 페이지 데이터가 setNotices(prev => [...prev, ...page.items])로 새 목록 뒤에 중복 추가될 수 있습니다.

또한 isLoadingMore를 의존성 배열에서 제거할 수 있습니다.
ref로 가드하므로 state 변경에 따른 handleLoadMore 재생성이 불필요해집니다.

L106–L112 기존 코드:

  const handleLoadMore = useCallback(() => {
    if (!hasNext || !nextCursor || isInitialLoading || isRefreshing || isLoadingMore) {
      return;
    }
    void loadNotices({ cursor: nextCursor });
  }, [hasNext, isInitialLoading, isLoadingMore, isRefreshing, loadNotices, nextCursor]);

수정 제안:

  const handleLoadMore = useCallback(() => {
    if (!hasNext || !nextCursor || isInitialLoading || isRefreshing || isLoadingMoreRef.current) {
      return;                               // ref로 즉시 가드
    }
    isLoadingMoreRef.current = true;        // 동기적으로 즉시 잠금
    activeRequestRef.current?.abort();
    const controller = new AbortController();
    activeRequestRef.current = controller;
    void loadNotices({ cursor: nextCursor, signal: controller.signal });
  }, [hasNext, isInitialLoading, isRefreshing, loadNotices, nextCursor]);
  // isLoadingMore 의존성 제거

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

피드백 감사합니다!

해당 내용은 앞선 요청 중복 방지 작업에 포함해서 반영했습니다.

handleLoadMore에서 기존 isLoadingMore state 기반 가드를 제거하고, isLoadingMoreRef.current로 즉시 중복 진입을 막도록 변경했습니다. 그래서 같은 렌더 사이클 안에서 onEndReached가 연속으로 호출되더라도 같은 cursor 요청이 중복 실행되지 않도록 했습니다.

또한 loadNotices({ cursor: nextCursor })를 직접 호출하지 않고 startLoadNotices({ cursor: nextCursor })를 통해 실행하도록 변경했습니다. startLoadNotices 내부에서 AbortController를 생성하고 signal을 전달하므로, 페이지 요청도 취소 가능하도록 처리되어 있습니다.

추가로 isLoadingMore는 더 이상 handleLoadMore 내부에서 참조하지 않기 때문에 의존성 배열에서도 제거했습니다.

void loadNotices({ cursor: nextCursor });
}, [hasNext, isInitialLoading, isLoadingMore, isRefreshing, loadNotices, nextCursor]);

const renderNotice = useCallback(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

renderNotice border 로직 변경 (L114–L138, L165–L193)

원인

renderNotice는 notices.length를 클로저로 캡처하고 index < notices.length - 1 조건으로 마지막 항목을 제외한 나머지에 구분선을 그립니다.

페이지네이션으로 새 항목이 추가되면 setNotices가 호출되고, 이 시점에 notices.length가 변경되어 useCallback([notices.length])가 새 함수를 만듭니다.
그런데 React와 FlatList의 렌더 사이클 특성상 이전 renderNotice 클로저가 살아있는 짧은 구간이 존재합니다.

예: 첫 페이지 20개 로드 후 다음 페이지 20개가 추가될 때

  • 이전 클로저 기준: notices.length = 20, index 19인 항목은 19 < 19 = false → 구분선 없음 (마지막 항목이므로 정상)
  • 새 클로저 기준: notices.length = 40, index 19인 항목은 19 < 39 = true → 구분선이 있어야 함
  • 하지만 FlatList가 해당 셀을 새 renderItem으로 아직 재렌더하지 않은 순간: 구분선 누락

ItemSeparatorComponent를 사용하면 FlatList가 항목 사이에 직접 구분선 컴포넌트를 렌더하므로 index나 notices.length 참조가 전혀 필요 없습니다.
마지막 항목 뒤에는 자동으로 렌더되지 않습니다.

L114–L138 기존 코드:

  const renderNotice = useCallback(
    ({ item, index }: { item: NoticeListItemResponse; index: number }) => (
      <TouchableOpacity
        style={[styles.noticeItem, index < notices.length - 1 && styles.noticeItemBorder]}
        ...
      >
    ),
    [notices.length],
  );

수정 제안:

  const renderNotice = useCallback(
    ({ item }: { item: NoticeListItemResponse }) => (
      <TouchableOpacity
        style={styles.noticeItem}   // border 조건 제거
        ...
      >
    ),
    [],   // 의존성 없음
  );

FlatList에 ItemSeparatorComponent 추가 (L179 근처):

  // L165 <FlatList props에 추가
  ItemSeparatorComponent={() => <View style={styles.itemSeparator} />}

추가적으로 이 제안을 받아드리시게 되면 styles 업데이트 (L261–L264)도 진행해야 합니다.

noticeItemBorder 삭제, itemSeparator 추가:

  // L261–L264 기존 (삭제)
  noticeItemBorder: {
    borderBottomWidth: 1,
    borderBottomColor: Colors.brand.line,
  }, 
  // 수정 후 (대체)
  itemSeparator: {
    height: 1,
    backgroundColor: Colors.brand.line,
    marginHorizontal: 18,
  },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

피드백 감사합니다!

말씀해주신 방식이 더 타당하다고 판단해 반영했습니다. 기존에는 renderNoticenotices.length를 클로저로 참조해서 index < notices.length - 1 조건으로 border를 그렸는데, 페이지네이션으로 항목이 추가될 때 기존 마지막 항목이 중간 항목으로 바뀌는 과정에서 구분선 렌더링이 일시적으로 어긋날 수 있다고 봤습니다.

그래서 구분선을 item 내부 스타일에서 계산하지 않고, FlatListItemSeparatorComponent로 분리했습니다. 이에 따라 renderNotice에서는 indexnotices.length 의존성을 제거했고, noticeItemBorder 스타일은 삭제한 뒤 itemSeparator 스타일을 추가했습니다.

이제 항목 사이 구분선은 FlatList가 직접 렌더링하므로 pagination 이후에도 마지막 항목 여부를 각 item에서 계산하지 않아도 됩니다.

Comment thread app/(tabs)/(home)/notices.tsx Outdated
<View style={styles.centerContent}>
<Text style={styles.errorTitle}>공지사항을 불러올 수 없습니다.</Text>
<Text style={styles.stateText}>{errorMessage}</Text>
<Button label="다시 시도" variant="secondary" onPress={() => void loadNotices()} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

재시도 버튼 AbortSignal 추가 (L159)

원인

에러 화면에서 "다시 시도"를 빠르게 두 번 탭하면 두 요청이 동시에 실행됩니다. signal이 없어 이전 요청을 취소할 수 없으므로 두 응답이 모두 완료되고, 나중에 완료된 응답의 setNotices(page.items)가 먼저 성공한 응답을 덮어씁니다. 네트워크 지연 상황에 따라 더 오래된 데이터나 에러 상태가 최종으로 남을 수 있습니다.

L159 기존 코드:

  <Button label="다시 시도" variant="secondary" onPress={() => void loadNotices()} />

수정 제안:

  <Button
    label="다시 시도"
    variant="secondary"
    onPress={() => {
      activeRequestRef.current?.abort();        // 진행 중인 요청 취소
      const controller = new AbortController();
      activeRequestRef.current = controller;
      void loadNotices({ signal: controller.signal });
    }}
  />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

피드백 감사합니다.

해당 내용은 앞선 요청 중복 방지 작업에 포함해서 반영해두었습니다. 기존에는 재시도 버튼에서 loadNotices()를 직접 호출했지만, 현재는 handleRetry를 통해 startLoadNotices()를 실행하도록 변경했습니다.

startLoadNotices 내부에서 진행 중인 요청을 activeRequestRef.current?.abort()로 취소한 뒤 새 AbortController를 생성하고, 해당 signalloadNotices에 전달합니다. 따라서 재시도 버튼을 빠르게 여러 번 눌러도 이전 요청의 지연 응답이 최신 상태를 덮어쓰는 상황을 방지할 수 있습니다.

Comment thread app/(tabs)/(home)/notices.tsx Outdated
}, [loadNotices]);

const handleLoadMore = useCallback(() => {
if (!hasNext || !nextCursor || isInitialLoading || isRefreshing || isLoadingMore) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

retryControllerRef 추가 + 재시도 버튼 수정 (L26, L107)

원인

notices.tsx의 재시도 버튼과 동일한 문제입니다. 현재 loadNotice()를 signal 없이 호출하므로:

 1. 빠른 두 번 탭: 두 요청이 동시 실행, 나중에 완료된 결과(setState({ status: 'success' }) 또는 setState({ status: 'error' }))가 먼저 완료된 결과를 덮어쓴다.
  2. 요청 중 뒤로 이동: Stack 화면(notice-detail.tsx)은 뒤로 가기 시 언마운트된다. useEffect cleanup에서 원래 컨트롤러는 abort되지만, 재시도 버튼으로 시작한 요청은 signal이 없어
  취소되지 않는다. 요청이 완료되면 setState가 호출되는데, React 18에서는 이것이 무시되지만 Expo Router가 화면을 스택에 캐시하는 경우 의도치 않은 상태 변경이 발생할 수 있다.
  // L26 기존
  const getTokenRef = useRef(getToken);
  // ↓ 추가
  const retryControllerRef = useRef<AbortController | null>(null);
  // L107 기존
  <Button label="다시 시도" variant="secondary" onPress={() => void loadNotice()} />
  // 수정 후
  <Button
    label="다시 시도"
    variant="secondary"
    onPress={() => {
      retryControllerRef.current?.abort();        // 이전 재시도 취소
      const controller = new AbortController();
      retryControllerRef.current = controller;
      void loadNotice(controller.signal);
    }}
  />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

피드백 감사합니다!

말씀해주신 내용이 타당하다고 판단해 반영했습니다. 기존에는 상세 화면의 재시도 버튼에서 loadNotice()를 signal 없이 직접 호출하고 있어서, 빠르게 여러 번 재시도하거나 화면을 이탈하는 경우 이전 요청을 취소할 수 없었습니다.

이를 막기 위해 retryControllerRef를 추가했고, 재시도 버튼은 handleRetry를 통해 실행되도록 변경했습니다. handleRetry에서는 기존 재시도 요청을 먼저 abort한 뒤 새 AbortController를 생성하고, 해당 signalloadNotice에 전달하도록 했습니다.

또한 화면 cleanup 시 최초 로드 요청뿐 아니라 재시도 요청도 함께 abort되도록 처리했습니다.

@sunm2n

sunm2n commented May 23, 2026

Copy link
Copy Markdown
Contributor

변경 모두 확인했습니다 수고하셨습니다

@kbh0218 kbh0218 merged commit 7d49cc7 into dev May 23, 2026
@kbh0218 kbh0218 deleted the feat/#47-notice-api branch May 23, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 기능개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] 공지사항 API 연동

2 participants