2024년 12월 작업

김의석 ·2024년 12월 5일

Hello! Poko Ver.2

목록 보기
19/28

POST 요청 구조와 GET 요청 응답 구조 통일

문제

  • 문제 요약:
    POST 요청과 GET 요청의 응답 구조 다른 것을 확인하고 추후 용이한 유지보수를 위해 기존 GET 요청의 응답 구조를 수정한다.

  • 오류 메시지: 없음.

문제 해결 과정

  1. 각 요청의 구조 확인

POST 요청 구조

{
  "date": "2024-10-25",
  "attendance": [
    { "id": 54, "attendance": true },
    { "id": 55, "attendance": false }
  ]
}

장점

  • 직관적이고 가독성이 높음: 각 학생의 ID와 출석 여부가 명시되어 있어, 데이터를 사람이 이해하기 쉽고 직관적이다.
  • 유연성: 다른 정보(예: 학생 이름, 반, 학년 등)를 추가할 때 구조를 쉽게 확장할 수 있다.

단점

  • 액세스 속도: 특정 학생의 출석 여부를 찾기 위해서는 배열을 순차적으로 검색해야 하므로 성능이 저하될 수 있다.
  • 데이터 중복 가능성: 중복된 ID가 포함될 수 있어 별도의 검증이 필요하다.

GET 요청의 응답 구조

{
  "date": "2024-09-01",
  "attendance": {
    "54": true,
    "55": false,
    "56": false
    //...
  }
}

장점

  • 데이터 액세스가 빠름: 학생의 출석 여부를 학생 ID로 바로 찾을 수 있어, 클라이언트나 서버에서 처리시 이점이 있다.
  • 데이터 중복을 피할 수 있음: ID를 키로 사용하므로 중복된 학생 데이터를 허용하지 않는다.

단점

  • 유연성 감소: 만약 ID 외의 정보를 같이 보낼 경우 확장성에 한계가 있다.
  • 데이터 직관성이 떨어짐: ID를 바로 사용하는 구조는 직관성이 약간 떨어져, 데이터를 제 3자가 읽기 힘들 수 있다.
  1. 가독성과 유연성을 위해 데이터 구조를 POST 요청 구조로 통일하고 GET 요청의 응답 API 코드 수정
    def list(self, request, *args, **kwargs):
        queryset = self.get_queryset()
        # attendance_data의 초기화 변경 Dict -> List
        attendance_data = defaultdict(list)

        for record in queryset:
            date_str = record.date.strftime("%Y-%m-%d")
            attendance_data[date_str].append(
                {"id": record.name.id, "attendance": record.attendance}
            )

        response_data = [
            {"date": date, "attendance": attendance}
            for date, attendance in attendance_data.items()
        ]

        return Response(response_data)

주요 수정 사항

  • defaultdict(list)으로 attendance_data의 초기화 변경
  • attendance_data[date_str]가 리스트로 초기화되어 .append()를 통해 데이터를 추가

defaultdict()의 동작 구조와 list 옵션

일반적인 딕셔너리와는 달리, 키가 존재하지 않을 때 기본값을 자동으로 생성한다.

list는 default_factory로 설정된 함수이다. 즉, attendance_data에 키가 없을 경우, 그 키에 대한 값으로 자동으로 빈 리스트 ([])를 생성한다.

from collections import defaultdict

# list로 초기화
attendance_data = defaultdict(list)

# 키가 존재하지 않을 때
# 빈 리스트 []가 자동 생성
attendance_data["2024-12-05"]  

# .append()로 데이터 추가
attendance_data["2024-12-05"].append({"id": 1, "attendance": True})

# 결과
{
    "2024-12-05": [{"id": 1, "attendance": True}]
}


  1. 수정된 응답구조에 맞춰 React의 코드 수정

수정 전 코드


const dataSource = useMemo(() => {
  return students.map(student => {
    const studentAttendance = data.reduce((acc, dateEntry) => {
	  // 수정 전
      acc[dateEntry.date] = dateEntry.attendance[student.id];
      return acc;
    }, {});
    return {
      key: student.id,
      name: student.name,
      ...studentAttendance,
    };
  });
}, [data, students]);
  • attendance 데이터가 {학생ID: 출석여부} 형태로 가정하고 student.id를 직접 키로 접근한다.

수정 후 코드

const dataSource = useMemo(() => {
  return students.map(student => {
    const studentAttendance = data.reduce((acc, dateEntry) => {
      // 수정 후
      const attendanceRecord = dateEntry.attendance.find(
        item => item.id === student.id
      );
      acc[dateEntry.date] = attendanceRecord ? attendanceRecord.attendance : null;
      return acc;
    }, {});
    return {
      key: student.id,
      name: student.name,
      ...studentAttendance,
    };
  });
}, [data, students]);
  • attendance 데이터가 배열임을 반영하여 find 메서드를 사용, student.id와 일치하는 출석 데이터를 검색한다.

dateEntry의 동작

  • data의 구조
[
  {
    date: "2024-12-01",
    attendance: [
      { id: 54, attendance: true },
      { id: 55, attendance: false },
    ],
  },
  {
    date: "2024-12-02",
    attendance: [
      { id: 54, attendance: false },
      { id: 55, attendance: true },
    ],
  },
]
  • data.map(dateEntry => ...)에서 dateEntrydata 배열의 각 요소를 하나씩 순회하며 생성합니다.
  • Column을 생성
...data.map(dateEntry => ({
  title: dateEntry.date,  // 날짜를 Column의 제목으로 사용
  dataIndex: dateEntry.date,  // 각 Column에서 데이터를 참조하는 키
  key: dateEntry.date,  // 고유 식별자
  width: 100,
  render: attendance => (  // attendance 데이터를 표시
    <span>
      {attendance ? '✅' : '❌'}
    </span>
  ),
}))
  • 학생별 출석 데이터 매핑
const studentAttendance = data.reduce((acc, dateEntry) => {
  const attendanceRecord = dateEntry.attendance.find(
    item => item.id === student.id
  );
  acc[dateEntry.date] = attendanceRecord ? attendanceRecord.attendance : null;
  return acc;
}, {});

동작요약

  • columns: 열 제목과 데이터를 렌더링하는 방식을 정의.
  • dataSource: 각 행에 표시될 데이터를 정의.
  • 매핑 방식: columns.dataIndex와 dataSource의 키가 일치하면 데이터가 테이블에 표시.

PATCH React 동작 정리

문제

  • 문제 요약: PATCH React 동작 정리

  • 오류 메시지: 없음.

문제 해결 과정

  1. 사용자가 날짜를 클릭 (openModal 함수 호출)
// AttendanceChart.js
 <a
  onClick={() => openModal(dateEntry.date, "edit")}
  style={{ cursor: "pointer", color: "#1890ff" }}
    >
      {dateEntry.date}
  </a>
  1. openModal
const openModal = (date, mode = "create") => {
  setselectedDate(date); // 선택된 날짜를 상태로 저장
  setModalMode(mode);    // 모달 모드를 설정: "edit" 또는 "create"

  if (mode === "edit") {
    // 선택된 날짜의 출석 데이터를 찾음
    const existingAttendance = attendanceData.find(
      (entry) => entry.date === date
    );

    if (existingAttendance) {
      // 출석 상태가 true인 학생의 ID만 가져와 checkedStudents 상태로 설정
      setCheckedStudents(
        existingAttendance.attendance
          .filter((entry) => entry.attendance)
          .map((entry) => entry.id)
      );
    }
  } else {
    setCheckedStudents([]); // "create" 모드일 경우 모든 체크박스를 초기화
  }
  setIsModalOpen(true); // 모달 열기
};

추가. AttendanceModal.js 동작원리 되짚기

checkedStudents의 데이터 구조

[studentId1, studentId2, studentId3, ...]

handlecheck.js

const handleCheck = (studentId) => {
      setCheckedStudents(prevChecked =>
        // 현재 ID가 이미 체크되어 있는지 확인
        prevChecked.includes(studentId)
		// 체크 해제: ID 제거
        ? prevChecked.filter(id => id !== studentId)
        // 체크: ID 추가
        : [...prevChecked, studentId]
      );
    };

AttendanceModal.js의 handleCheck

 <Checkbox
  checked={checkedStudents.includes(student.id)}
  onChange={() => handleCheck(student.id)}
    >
      {student.name}
</Checkbox>

예시

초기 상태

checkedStudents = [1, 3];
ID 3 체크 해제 (이미 존재)
prevChecked.includes(3)true

prevChecked.filter((id) => id !== 3); // 결과: [1]
업데이트 된 상태
checkedStudents = [1];
ID 2 체크 추가 (존재하지 않음)
prevChecked.includes(2)false

[...prevChecked, 2]; // 결과: [1, 2]
업데이트 된 상태
checkedStudents = [1, 2];

PATCH React API 제작

문제

  • 문제 요약:
    PACT React API 제작 및 관련 handle 함수를 수정 함.

  • 오류 메시지: 없음

문제 해결 과정

  1. API 제작 후 handle 함수를 edit과 credit 모드에 따라 다르게 작동하게 함.
const handleSubmit = async () => {
    const attendanceData = students.map((student) => ({
        id: student.id,
        attendance: checkedStudents.includes(student.id),
    }));

    try {
        if (modalMode === "edit") {
            // PATCH 요청
            await patchAttendanceData(selectedDate, attendanceData); // 수정된 데이터를 서버로 전송
        } else {
            // POST 요청
            await postAttendanceData(getFormattedDate(new Date()), attendanceData); // 새로운 데이터를 서버로 전송
        }

        // 새로운 데이터를 반영하기 위해 출석 데이터를 다시 가져옴
        const updatedAttendanceData = await fetchAttendanceData(selectedYear);

        // 차트를 업데이트하기 위해 상태를 업데이트
        setAttendanceData(updatedAttendanceData);
    } catch (생략)

    setIsModalOpen(false); // 모달 닫기
};

Django Admin CSRF 오류

문제

  • 문제 요약: 배포환경에서 Admin 로그인 시 Request Origin 헤더가 null로 전달되며 CSRF 검증 실패 함.

  • 오류 메시지: Forbidden (403)

문제 해결 과정

  1. Nginx Referrer-Policy 설정 확인. no-referrer를 제거하거나, 적절히 수정하여 Referrer가 올바르게 전달되도록 변경.
# nginx.conf
# 수정 후
	add_header Referrer-Policy "strict-origin-when-cross-origin";
    
# 수정 전
    add_header Referrer-Policy "no-referrer";

"strict-origin-when-cross-origin" 수정하여 Referrer(Origin 헤더)가 올바르게 전달되도록변경 함.

참고

SameSite 옵션 정리

현재 배포환경 구조에서 아래 옵션이 가지는 의미를 정리한다.

CSRF_COOKIE_SAMESITE = "None"
SESSION_COOKIE_SAMESITE = None"

CSRF_COOKIE 또는 SESSION_COOKIE를 도메인 A에서 도메인 B로 전송하도록 허용한다.

# 도메인 A, nginx
location / {
    root /usr/share/nginx/html;
    try_files $uri $uri/ /index.html =404;
}

React 프론트엔드이며 poko-dev.com에서 정적 파일(index.html)을 제공한다.

# 도메인 B, django 백엔드 서버
upstream poko {
    server web:8000;
}

# Django admin 프록시
    location /admin/ {
        proxy_pass http://web:8000/admin/;  # Django admin 페이지로 프록시
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header Origin $http_origin;  # Origin 헤더 추가
        proxy_redirect off;
    }

Django 백엔드(API)이며 /api/와 /admin/ 경로는 poko-dev.com에서 프록시를 통해 Django 컨테이너(web:8000)로 전달한다. Nginx가 Django로 요청을 전달할 때 이 upstream을 사용한다.

[React 프론트엔드: poko-dev.com]
  location /
    ↓ (정적 파일 제공)
[Static files: /usr/share/nginx/html]

API 호출 (프록시)
    ↓
[Nginx]
  location /api/      → Django API 프록시로 전달
  location /admin/    → Django Admin 프록시로 전달
    ↓
[Django 백엔드 컨테이너: web:8000]

SameSite 옵션

  1. SameSite=Strict:
  • 동일 사이트에서만 쿠키를 전송.
  • 다른 도메인(또는 서브도메인)에서 발행된 요청에는 쿠키를 포함하지 않음.
  • 사용 사례: 민감한 데이터 보호가 필요한 환경.
  1. SameSite=Lax:
  • GET 요청은 쿠키가 전송되지만, POST, PUT, DELETE와 같은 변경 요청에는 전송되지 않음.
  • 사용 사례: 적당한 보안과 편의성을 모두 고려한 환경.
  1. SameSite=None:
  • 모든 요청에서 쿠키를 전송(다른 도메인 포함).
  • 단, 쿠키는 Secure 플래그가 설정된 경우에만 사용 가능(HTTPS 필요).
  • 사용 사례: 크로스 오리진 요청(CORS)이 필요한 환경.

React Attendance Chart UI 이슈

문제

  • 문제 요약: React Table에서 날짜 컬럼의 데이터 수에 따라 테이블의 컬럼 폭이 일정하지 않음.

  • 오류 메시지: 없음

문제 해결 과정

  1. react Table의 'max-content'으로 수정.
<Table
   columns={columns} 
   dataSource={dataSource} 
   pagination={false} 
   scroll={{ x: 'max-content' }} // 컬럼 내용에 따라 스크롤 크기 조정
   sticky
   bordered
   />;
  • Table의 모든 컬럼의 width 속성을 기반으로 전체 테이블의 총 너비(가로)를 계산한다.
  • 테이블의 총 너비가 부모 컨테이너의 가로 폭보다 크다면, 자동으로 가로 스크롤이 활성화한다.
  1. 동작
  • 여러 날짜 컬럼이 추가되면, 테이블의 전체 너비가 늘어나고 부모 컨테이너 너비를 초과하면 가로 스크롤이 표시된다.
  • 날짜 컬럼이 1~2개일 때, 테이블의 전체 너비가 부모 컨테이너 내에 포함되므로 스크롤이 생성되지 않는다.

URL Routing Issue(APPEND_SLASH)

문제

  • 문제 요약: Django에서 URL 라우팅 관련하여 404 에러가 발생

  • 오류 메시지: 404 Page Not Found

문제 해결 과정

  1. path() 에서 /의 위치를 뒤로 수정 함, Django에서 권장 방식 끝에 / 포함.
path("homepage_attendance/", ...)

APPEND_SLASH

URL 끝에 /가 없는 요청을 자동으로 리다이렉트하여 끝에 /가 있는 URL로 매칭하도록 도와준다.

APPEND_SLASH = True

# 요청 URL: /homepage_attendance
# URL 패턴: path("homepage_attendance/", ...)
  • Django가 요청 URL을 /homepage_attendance/로 리다이렉트.
  • 매칭 성공: 200 OK.
APPEND_SLASH = False

# 요청 URL: /homepage_attendance
# URL 패턴: path("homepage_attendance/", ...)
  • 요청 URL과 URL 패턴이 매칭되지 않음.
  • 결과: 404 Page Not Found.

ORM 역참조

문제

  • 문제 요약: 로그인 한 유저의 조회한 날짜(=주일), 반 출석/결석을 표시하는 기능 구현을 위해 ORM 역참조를 사용한다.

  • 오류 메시지: 없음.

문제 해결 과정

  1. self.request.user
    self.request.user는 Django의 AuthenticationMiddleware와 SessionMiddleware가 활성화된 경우, 요청한 사용자의 정보를 담는다.
# settings.py

MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
]
  1. 현재 poko 프로젝트에서는 기본 사용자 모델CustomUser로 대체하였다.
# settings.py

AUTH_USER_MODEL = "accounts.CustomUser"

# accounts의 models.py

class CustomUser(AbstractUser):
    username = None
    email = models.EmailField(_("email address"), unique=True)
    full_name = models.CharField(max_length=4)
    birth_date = models.DateField(null=True, blank=True)
    registration_number = models.CharField(max_length=6, null=True, blank=True

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    objects = UserManager()

즉, self.request.user는 CustomUser 모델의 모든 필드에 접근할 수 있다.

  1. 역참조

Django의 역참조는 외래 키(ForeignKey)가 정의된 모델에서 설정된 related_name을 기반으로 동작한다.

# attendance의 model.py

class Member(models.Model):
    # to_field : 외래키로 지정된 모델의 특정 필드값을 참조할 수 있게함.
    teacher = models.ForeignKey(
        CustomUser,
        on_delete=models.CASCADE,
        related_name="members",
    )
    name = models.CharField(max_length=5, unique=True)
    grade = models.CharField(max_length=3, null=True, default=None)
    gender = models.CharField(max_length=3, null=True, default=None)
    attendance_count = models.IntegerField(default=0)
    absent_count = models.IntegerField(default=0)

related_name="members"는 CustomUser 객체에서 자신과 연결된 Member 객체를 가져올 때 사용한다.

# 예시

user = CustomUser.objects.get(email="example@example.com")
members = user.members.all()  # CustomUser 객체에서 역참조로 Member 객체 조회

print(members)  # [<Member: Student1>, <Member: Student2>]
  1. Attendance.objects.filter(name__in=members)

name__in=members는 Django ORM의 필터링 옵션이며 __in은 특정 쿼리셋 또는 리스트의 값을 기준으로 필터링할 때 사용한다.

attendance_records = Attendance.objects.filter(name__in=members, date="2024-12-17")

# 결과
# [<Attendance: Member1>, <Attendance: Member2>]

React Homepage API 및 Component 완성

문제

  • 문제 요약:

  • 오류 메시지:

문제 해결 과정

  1. 과정 서술

dateUtils 생성 및 코드 정리

문제

  • 문제 요약:

  • 오류 메시지:

문제 해결 과정

  1. 과정 서술

AttendanceChart 날짜 정렬 오류 수정

문제

  • 문제 요약:

  • 오류 메시지:

문제 해결 과정

  1. 과정 서술

제목

문제

  • 문제 요약:

  • 오류 메시지:

문제 해결 과정

  1. 과정 서술

profile
널리 이롭게

0개의 댓글