문제 요약:
POST 요청과 GET 요청의 응답 구조 다른 것을 확인하고 추후 용이한 유지보수를 위해 기존 GET 요청의 응답 구조를 수정한다.
오류 메시지: 없음.
{
"date": "2024-10-25",
"attendance": [
{ "id": 54, "attendance": true },
{ "id": 55, "attendance": false }
]
}
{
"date": "2024-09-01",
"attendance": {
"54": true,
"55": false,
"56": false
//...
}
}
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)
일반적인 딕셔너리와는 달리, 키가 존재하지 않을 때 기본값을 자동으로 생성한다.
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}]
}
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]);
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]);
[
{
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 => ...)에서 dateEntry는 data 배열의 각 요소를 하나씩 순회하며 생성합니다....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;
}, {});
동작요약
문제 요약: PATCH React 동작 정리
오류 메시지: 없음.
// AttendanceChart.js
<a
onClick={() => openModal(dateEntry.date, "edit")}
style={{ cursor: "pointer", color: "#1890ff" }}
>
{dateEntry.date}
</a>
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 동작원리 되짚기
[studentId1, studentId2, studentId3, ...]
const handleCheck = (studentId) => {
setCheckedStudents(prevChecked =>
// 현재 ID가 이미 체크되어 있는지 확인
prevChecked.includes(studentId)
// 체크 해제: ID 제거
? prevChecked.filter(id => id !== studentId)
// 체크: ID 추가
: [...prevChecked, studentId]
);
};
<Checkbox
checked={checkedStudents.includes(student.id)}
onChange={() => handleCheck(student.id)}
>
{student.name}
</Checkbox>
checkedStudents = [1, 3];
prevChecked.includes(3) → true
prevChecked.filter((id) => id !== 3); // 결과: [1]
checkedStudents = [1];
prevChecked.includes(2) → false
[...prevChecked, 2]; // 결과: [1, 2]
checkedStudents = [1, 2];
문제 요약:
PACT React API 제작 및 관련 handle 함수를 수정 함.
오류 메시지: 없음
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); // 모달 닫기
};
문제 요약: 배포환경에서 Admin 로그인 시 Request Origin 헤더가 null로 전달되며 CSRF 검증 실패 함.
오류 메시지: Forbidden (403)
# nginx.conf
# 수정 후
add_header Referrer-Policy "strict-origin-when-cross-origin";
# 수정 전
add_header Referrer-Policy "no-referrer";
"strict-origin-when-cross-origin" 수정하여 Referrer(Origin 헤더)가 올바르게 전달되도록변경 함.
현재 배포환경 구조에서 아래 옵션이 가지는 의미를 정리한다.
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]
문제 요약: React Table에서 날짜 컬럼의 데이터 수에 따라 테이블의 컬럼 폭이 일정하지 않음.
오류 메시지: 없음
'max-content'으로 수정.<Table
columns={columns}
dataSource={dataSource}
pagination={false}
scroll={{ x: 'max-content' }} // 컬럼 내용에 따라 스크롤 크기 조정
sticky
bordered
/>;
문제 요약: Django에서 URL 라우팅 관련하여 404 에러가 발생
오류 메시지: 404 Page Not Found
/의 위치를 뒤로 수정 함, Django에서 권장 방식 끝에 / 포함.path("homepage_attendance/", ...)
URL 끝에 /가 없는 요청을 자동으로 리다이렉트하여 끝에 /가 있는 URL로 매칭하도록 도와준다.
APPEND_SLASH = True
# 요청 URL: /homepage_attendance
# URL 패턴: path("homepage_attendance/", ...)
APPEND_SLASH = False
# 요청 URL: /homepage_attendance
# URL 패턴: path("homepage_attendance/", ...)
문제 요약: 로그인 한 유저의 조회한 날짜(=주일), 반 출석/결석을 표시하는 기능 구현을 위해 ORM 역참조를 사용한다.
오류 메시지: 없음.
self.request.userself.request.user는 Django의 AuthenticationMiddleware와 SessionMiddleware가 활성화된 경우, 요청한 사용자의 정보를 담는다.# settings.py
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
]
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 모델의 모든 필드에 접근할 수 있다.
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>]
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>]
문제 요약:
오류 메시지:
문제 요약:
오류 메시지:
문제 요약:
오류 메시지:
문제 요약:
오류 메시지: