[nextjs]SNS Server-51(myapp61+)[Pagination+API] 

👉🏻 여기서는 페이지네이션을 구현할때 호출되는 API와 함께 페이지네이션 구조를 살펴 봅니다.
Here, we examine the pagination structure along with the API called when implementing pagination.

👉🏻 이전 포스트에서 ClientList.tsx에서 TimelineList.tsx로 초기데이터를 전달하면 이 데이터가 useCursonPagination훅에 들어갔다가 나옵니다.
As mentioned in the previous post, when initial data is passed from ClientList.tsx to TimelineList.tsx, it is processed by the useCursorPagination hook.

👉🏻 TimlineList.tsx를 통해 훅내부에 전달한 콜백함수인 fetcher가 훅 내부에서 실행되고 API와 통신하고 데이터를 받아와서 기존 데이터 뒤에 붙여줍니다.
The fetcher callback function, passed into the hook via TimelineList.tsx, executes within the hook, communicates with the API, retrieves data, and appends it to the existing data.

📁 콜백함수 fetcher와 API 통신 / API communication using the fetcher callback function

✔️ app/api/v1/timelines/[type]/route.ts

✔️ API와 통신할때 사용되는 URL 형식 / URL format used when communicating with an API

GET /api/v1/timelines/home?limit=20&max_id=59d38066-6cc2-4c2b-b06e-710f7293fda6
GET /api/v1/timelines/home?limit=20&max_id=41b5b137-d56c-4e36-a995-3dbed5ef061c
GET /api/v1/timelines/home?limit=20&max_id=a864ab3b-a090-4575-a7b1-0bf5c075f73b

✔️ TimelineList.tsx의 fetcher콜백함수는 아래아 같이 API와 통신을 합니다.
The fetcher callback function in TimelineList.tsx communicates with the API as follows.

      if (tab === 'home') {
        if (effectiveUser) {
          url = `/api/v1/timelines/home?${params.toString()}`;
        } else {
          params.set('local', 'true');
          url = `/api/v1/timelines/public?${params.toString()}`;
        }
      } else if (tab === 'local') {
        params.set('local', 'true');
        url = `/api/v1/timelines/public?${params.toString()}`;
      } else {
        url = `/api/v1/timelines/public?${params.toString()}`;
      }

1)이 params.toString()이 ‘limit=20&max_id=a864ab3b-a…’ 이 부분입니다.
This params.toString() corresponds to the part that looks like 'limit=20&max_id=a864ab3b-a...'.

✔️ API내부에서 다음과 같은 과정으로 전달 받은 파라메터를 처리합니다.
The API processes the received parameters internally through the following steps.

1)데이터 검색 / Data Retrieval

  let filteredRows: any[] = [];

  if (type === 'home') {
    // ✅ myapp61+ - LIMIT 200 추가 / Add LIMIT 200
    const myPosts = db.prepare(`SELECT * FROM posts WHERE username =? LIMIT 200`).all(viewer) as any[];
    const inboxPosts = db.prepare(`SELECT * FROM inbox_posts WHERE username =? LIMIT 200`).all(viewer) as any[];as any[];
... ...
  } else if (type === 'public') {
... ...
  } else { ... ...}

A. 각 탭에 따라서 필요한 데이터를 검색합니다
Search for the necessary data based on each tab.

B. API에서 탭은 URL에서 … timelines/public.. ,…timeline/home…에서 public,home 부분입니다.
In the API, the “tabs” refer to the public and home segments found in URL paths like .../timelines/public... and .../timeline/home....

2)max_id,limit

  if (max_id) {
    // 현재는 타임스탬프가 아닌 UUID들어가는 구조라 maxTime은 항상 0.
    // Currently, since the structure uses a UUID instead of a timestamp, maxTime is always 0.
    let maxTime = parseDate(max_id);
    
    // max_id가 UUID인 경우 created_at검색 / Search created_at when max_id is a UUID
    if (maxTime === 0) {
      const refRow = filteredRows.find((r: any) => String(r.id) === String(max_id));
      if (refRow) {
        // maxTime에 날짜 값 저장 이제 0이 아님 / Storing the date value in maxTime; it is no longer zero.
        maxTime = parseDate(refRow.created_at);
      } else {
        // filteredRows에 없으면 DB에서 직접 조회 / If not found in filteredRows, query the database directly.
        try {
          const refDb = db.prepare(`SELECT created_at FROM posts WHERE id=?`).get(max_id) as any;
          if (refDb) maxTime = parseDate(refDb.created_at);
          else {
            const refInbox = db.prepare(`SELECT created_at FROM inbox_posts WHERE id=?`).get(max_id) as any;
            if (refInbox) maxTime = parseDate(refInbox.created_at);
          }
        } catch {}
      }
    }

    // 이전글 검색로직 / Previous Post Search Logic
    // 전달받은 max_id와 일치하는 글보다 이전글을 배열에서 검색 / Search the array for posts preceding the post matching the provided max_id.
    if (maxTime > 0) {
      filteredRows = filteredRows.filter((r) => parseDate(r.created_at) < maxTime);
    }
  }

  // 파라메터로 전달받은 limit=20로 데이터 20개만 선택 
  // Select only 20 items using the limit=20 parameter
  const hasMore = filteredRows.length > limit;
  const sliced = filteredRows.slice(0, limit);


  const statuses = sliced.map((row: any) => { ... ... 
    return {  ... ...
    });
  }

A.max_id와 같은 글의 created_at(글 작성한 날자)를 검색해서 이 created_at보다 이전 데이터만 추출합니다.
Retrieve the created_at (post creation date) associated with the max_id, and extract only the data from before that timestamp.

B. 페이지네이션이 적용되기 전에 첫 게시물 20개는 order by desc로 가져오기 때문에 최신데이터부터 이전데이터 20개를 가져옵니다.
Before pagination is applied, the first 20 posts are retrieved using ORDER BY DESC, so the 20 most recent items are fetched.

C.여기서 마지막에 본 글의 아이디가 max_id가 되고 이 max_id와 같은 글의 id필드에 있는 crated_at(글이 작성된 날짜)이전의 데이터 20개를 가져와서 리턴합니다.
Here, the ID of the last post viewed becomes the max_id; the system then retrieves and returns 20 records that precede the created_at timestamp associated with that max_id.

📁 데이터 처리과정 요약 / Summary of the Data Processing Workflow

✔️ API부분의 데이터 처리 과정은 다음과 같습니다.
The data processing flow for the API component is as follows.

1)최신데이터 검색
Search for the latest data

2)max_id가 있으면 max_id같은 id가 있는 필드의 created_at을 검색
If max_id exists, search for the created_at of fields with an ID matching max_id.

3)limit수량(20개) 만큼만 데이터를 잘라서 리턴 
Slice the data up to the specified limit (20 items) and return it.

📁부스트 카운터(코드수정) / Boost Counter (Code Modification)

  const allAnnounces = viewer ? (db.prepare(`SELECT * FROM announces WHERE username =?`).all(viewer) as any[]) : [];
... ...

  const announcesByObject = new Map<string, number>();

  for (const a of allAnnounces) {
    /* 
    announces 갯수 세기
    - [fullurl,'/' 기준으로 전체 분리후 마지막 요소만 선택] => [https://fllurl/adc.abc]
    - allAnnounces가 10개면 for가 20번 돌게 되어 있음 k가 문자열이라 a.object1회,a.object.split가 2회 이렇게 for가 실행됨.
    */
    for (const k of [a.object, a.object.split('/').pop() || '']) { if (k) announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1); }
  }

A.기본적으로 전체 부스트(allAnnounces)에서 announcedByObject Map에 fullURL과 ‘/’기준으로 분리된 마지막url값(split(‘/’).pop())을 모두 저장
Basically, for all boosts (allAnnounces), store both the fullURL and the last URL segment (obtained via split('/').pop()) in the announcedByObject map.

B. cont k of [a.object, a.object.split(‘/’).pop()]은 k에 배열 값이 문자열로 각각 따로 할당되어 데이터가 10개면 for가 20번 실행되게 되어 있음.
In cont k of [a.object, a.object.split('/').pop()], the array values ​​are assigned to k individually as strings; consequently, if there are 10 data items, the for loop executes 20 times.

C.announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1); } 여기서 URL과 숫자를 Map에 입력함
announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1); } Here, the URL and the number are stored in the Map.

# 전체 데이터가 아래와 같다면... / If the entire dataset is as follows...
allAnnounces = [
  {object: "https://server.com/posts/abc123"},
  {object: "https://server.com/posts/abc123"},
  {object: "abc123"}
]

# 첫데이터 예시 / Example of initial data
1. a.object
   = "https://myserver.com/posts/abc123" (전체 URL / Full URL)

2. a.object.split('/').pop()
   = "https://server.com/posts/abc123".split('/')
   = ["https:", "", "server.com", "posts", "abc123"]
   .pop() = 마지막 요소/Last element, ID only = "abc123" (id만/ID only)

3.그러면 아래와 같이 배열에 저장됨 / Then, it is stored in an array as shown below.
[ a.object, a.object.split('/').pop() || '' ]
= [ "https://myserver.com/posts/abc123", "abc123" ]

4.for 문이 실행되면서 const k는 문자열이기 때문에 k = "https://myserver.com/posts/abc123" 이렇게 한번실행하고 두번째로 k = "abc123" 이렇게 할당되어 실행됨
As the `for` loop executes, since `k` is a string, it first runs with `k = "https://myserver.com/posts/abc123"`, and then, in the second iteration, it runs with `k = "abc123"` assigned to it.

5.announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1); 
- 여기서 announcesByObject맵에 k("https://myserver.com/posts/abc123") 값이 Map에 셋팅됨
Here, the key "https://myserver.com/posts/abc123" is set in the `announcesByObject` map.

- Map의 두번째 인자가 숫자이고 지금 아무것도 저장하지 않았으니 announcesByObject.get(k) 이건 기본값이 0이됨
Since the second argument of the Map is a number and nothing has been stored yet, `announcesByObject.get(k)` defaults to 0.

- 0에 + 1하면 값이 1 그래서 맵에는 ["https://myserver.com/posts/abc123",1] 이렇게 저장됨.
Adding 1 to 0 results in a value of 1, so it is stored in the map like this: `["https://myserver.com/posts/abc123", 1]`.

- 그리고 두번째 요소 "abc123"도 ["abc123",1] 이렇게 저장됨
And the second element, "abc123", is also stored as `["abc123", 1]`.




# 두번째 데이터 예시 / Second data example
1.첫데이터 예시의 1,2번의 과정이 동일하게 실행됨.
Steps 1 and 2 from the first data example are executed in the same way.

2.announcesByObject.set(k, (announcesByObject.get(k) || 0) + 1); 
- 여기서 맵에 기존에 저장했던 동일한 키가 "https://myserver.com/posts/abc123"이거고 기존값은 1
Here, the identical key previously saved in the map is "https://myserver.com/posts/abc123", and the existing value is 1.

- 그래서 기존키의 두번째 인자는 2가 된다.
Therefore, the second argument of the existing key becomes 2.

- ["https://myserver.com/posts/abc123",2] , ["abc123",2] 




# 세번째 데이터 예시 / Third data example
1.세번째 데이터는 "abc123"이 값만 있기때문에 중복 제거하면 기존에 "abc123"의 키에 +1이 됩니다.
Since the third data item consists only of the value "abc123," removing duplicates results in the count for the existing "abc123" key increasing by 1.

2.그러면 ["https://myserver.com/posts/abc123",2] ,["abc123",3] 이렇게 됩니다.
Then it becomes `["https://myserver.com/posts/abc123", 2], ["abc123", 3]`.

3.지금 호출방식이 전체 URL로 호출하므로 ["https://myserver.com/posts/abc123",2]  이걸 기준으로 사용하면됩니다.
Since the current invocation method uses the full URL, you can use `["https://myserver.com/posts/abc123", 2]` as the basis.


# 결국은 전체 url로 호출하던지 짧은 id로 호출하던지 둘다 대응하기 위한 로직이며 사용할때 둘중 하나만 사용할것.
# Ultimately, the logic is designed to handle calls made using either the full URL or the short ID; however, you should use only one of the two methods.

마태복음 21장 22절 / Matthew 21: 22

“너희가 기도할 때에 무엇이든지 믿고 구하는 것은 다 받으리라 하시니라”

“And everything you ask in prayer, believing, you will receive.”

Leave a Reply