From 239aaf8388769c75d5282dd2d5f2fd9b789073bb Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 11:35:07 +0900 Subject: [PATCH 01/13] =?UTF-8?q?refactor:=20=EA=B0=90=EC=A0=95=EB=B3=84?= =?UTF-8?q?=20=EA=B8=B0=EB=A1=9D=20=ED=83=AD=20=ED=85=9C=ED=94=8C=EB=A6=BF?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/templates/diary/list.html | 415 +----------------- .../diary/partials/emotion_list.html | 97 ++++ .../diary/partials/emotion_modal.html | 74 ++++ 3 files changed, 173 insertions(+), 413 deletions(-) create mode 100644 diary/templates/diary/partials/emotion_list.html create mode 100644 diary/templates/diary/partials/emotion_modal.html diff --git a/diary/templates/diary/list.html b/diary/templates/diary/list.html index 39224df..efd523d 100644 --- a/diary/templates/diary/list.html +++ b/diary/templates/diary/list.html @@ -1,341 +1,4 @@ - - -{% extends "base.html" %} -{% load static %} - -{% block title %}다이어리 목록 | HeartMark{% endblock %} - -{% block extra_css %} -{% comment %} -담당 C의 감정 아이콘 스타일을 불러옵니다. -{% endcomment %} - - -{% comment %} -다이어리 목록 페이지 전용 스타일을 불러옵니다. -{% endcomment %} - -{% endblock %} - - -{% block content %} -
-
- - -
- {% include "diary/partials/diary_tab.html" %} -
- - - -
- -
- - -
-

- 전체 기록 -

- - - {{ records|length }}개 - -
- - - -
- - - - - - - - - - - - - - -
-
- - - -
- {% for record in records %} - - {% comment %} - 실제 Record 모델에 맞춰 작성한 공통 카드를 불러옵니다. - - record=record - → 현재 반복 중인 기록 하나를 - diary_card.html에 전달합니다. - {% endcomment %} - - {% include "diary/partials/diary_card.html" with record=record %} - - {% empty %} - -
-

- 아직 작성한 기록이 없어요. -

- -

- 오늘의 마음을 첫 기록으로 남겨보세요. -

- - {% comment %} - 담당 C의 실제 기록 작성 페이지로 이동합니다. - {% endcomment %} - - 기록 작성하기 - -
- {% endfor %} -
-
- - - -
-
-
-

- 감정별 기록 -

- -

- 감정의 크기는 이번 달 기록 횟수를 나타내요. -

-
- -
- - 가장 많이 기록한 감정 - - - {% if top_emotions %} - - {% for emotion in top_emotions %} - - {{ emotion.name }} - - - {% if not forloop.last %} - - · - - {% endif %} - {% endfor %} - - {% else %} - - 아직 기록 없음 - - {% endif %} -
-
- - -
- {% for emotion in recorded_emotions %} - - - {% empty %} -

- 표시할 감정이 없어요. -

- {% endfor %} -
-
- - - +ㅊ====================== -->
- - +{% include "diary/partials/emotion_modal.html" %} +
+
+
+

+ 감정별 기록 +

+ +

+ 감정의 크기는 이번 달 기록 횟수를 나타내요. +

+
+ +
+ + 가장 많이 기록한 감정 + + + {% if top_emotions %} + + {% for emotion in top_emotions %} + + {{ emotion.name }} + + + {% if not forloop.last %} + + · + + {% endif %} + {% endfor %} + + {% else %} + + 아직 기록 없음 + + {% endif %} +
+
+ + +
+ {% for emotion in recorded_emotions %} + + + {% empty %} +

+ 표시할 감정이 없어요. +

+ {% endfor %} +
+
\ No newline at end of file diff --git a/diary/templates/diary/partials/emotion_modal.html b/diary/templates/diary/partials/emotion_modal.html new file mode 100644 index 0000000..2c49de4 --- /dev/null +++ b/diary/templates/diary/partials/emotion_modal.html @@ -0,0 +1,74 @@ + + \ No newline at end of file From a7f8e1d805e4b09f0d4f90e71ab3226c20386542 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 11:42:11 +0900 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20=EA=B0=90=EC=A0=95=EB=B3=84=20?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20include=20=EB=AC=B8=EB=B2=95=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/templates/diary/list.html | 359 +++++++++++++++++++++++++++++--- 1 file changed, 329 insertions(+), 30 deletions(-) diff --git a/diary/templates/diary/list.html b/diary/templates/diary/list.html index efd523d..5cc23e0 100644 --- a/diary/templates/diary/list.html +++ b/diary/templates/diary/list.html @@ -1,13 +1,300 @@ -ㅊ====================== --> + + +{% extends "base.html" %} +{% load static %} + + +{% block title %} +다이어리 목록 | HeartMark +{% endblock %} + + +{% block extra_css %} +{% comment %} +담당 C의 감정 아이콘 스타일을 불러옴 +{% endcomment %} + + +{% comment %} +다이어리 목록 페이지 전용 스타일을 불러옴 +{% endcomment %} + +{% endblock %} + + +{% block content %} +
+
+ + +
+ {% include "diary/partials/diary_tab.html" %} +
+ + + + + + + + + {% comment %} + 감정별 기록 탭의 전체 HTML은 + emotion_list.html에서 관리 + + emotion_list.html 내부에서 다음 변수를 그대로 사용 가능 + + - selected_tab + - records + - recorded_emotions + - top_emotions + {% endcomment %} + + {% include "diary/partials/emotion_list.html" %} + + +
+ + +{% comment %} +감정별 기록 팝업의 HTML은 +emotion_modal.html에서 관리 +{% endcomment %} + {% include "diary/partials/emotion_modal.html" %} @@ -177,12 +464,17 @@

aria-labelledby="location-modal-title" hidden > +
+ +
+ +

@@ -195,6 +487,8 @@

+ +
+
{% for record in records %} +
> {% include "diary/partials/diary_card.html" with record=record %}
+ {% endfor %}
+
{% block extra_js %} +{% endblock %} \ No newline at end of file From 094d84bec94a4ac12b610e5c1de950e2ec4f3748 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 11:47:20 +0900 Subject: [PATCH 03/13] =?UTF-8?q?refactor:=20=EA=B0=90=EC=A0=95=EB=B3=84?= =?UTF-8?q?=20=EA=B8=B0=EB=A1=9D=20=ED=83=AD=EC=9D=84=20=EB=8F=84=EB=84=9B?= =?UTF-8?q?=20=EA=B7=B8=EB=9E=98=ED=94=84=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../diary/partials/emotion_list.html | 301 ++++++++++++------ 1 file changed, 210 insertions(+), 91 deletions(-) diff --git a/diary/templates/diary/partials/emotion_list.html b/diary/templates/diary/partials/emotion_list.html index 73e1553..bd9b4ea 100644 --- a/diary/templates/diary/partials/emotion_list.html +++ b/diary/templates/diary/partials/emotion_list.html @@ -1,97 +1,216 @@ - +
+ +
+ +

감정별 기록 - ========================== --> -
+ +

+ 기록한 대표 감정을 한눈에 확인해보세요. +

+ +

+ +
+ + + +
+ + +
-
-
-

- 감정별 기록 -

- -

- 감정의 크기는 이번 달 기록 횟수를 나타내요. -

-
- -
- - 가장 많이 기록한 감정 - - - {% if top_emotions %} - - {% for emotion in top_emotions %} - - {{ emotion.name }} - - - {% if not forloop.last %} - - · - - {% endif %} - {% endfor %} - - {% else %} - - 아직 기록 없음 - - {% endif %} -
+ + + + + + + + + + + + + + +
+ + + 전체 기록 + + + + {{ records|length }}개 + +
-
- {% for emotion in recorded_emotions %} - - - {% empty %} -

- 표시할 감정이 없어요. -

- {% endfor %} + + + + + +
+ + + + + + + + + + {% if not recorded_emotions %} + +
+ +

+ 아직 기록된 감정이 없어요. +

+ +

+ 오늘의 대표 감정을 기록해보세요. +

+
- \ No newline at end of file + + {% endif %} + +
+ \ No newline at end of file From 1304d1cb91cf09a55c16187d9649cc4e5c4747f4 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 11:55:34 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=EB=8C=80=ED=91=9C=20=EA=B0=90?= =?UTF-8?q?=EC=A0=95=20=EC=83=89=EC=83=81=20=EA=B7=B8=EB=A3=B9=EB=B3=84=20?= =?UTF-8?q?=EA=B7=B8=EB=9E=98=ED=94=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/views.py | 680 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 545 insertions(+), 135 deletions(-) diff --git a/diary/views.py b/diary/views.py index 2272300..3b29f63 100644 --- a/diary/views.py +++ b/diary/views.py @@ -7,7 +7,7 @@ from django.shortcuts import get_object_or_404, render from django.utils import timezone -# 담당 C가 만든 실제 기록 모델을 가져옵니다. +# 담당 C가 만든 실제 기록 모델을 가져옴 from records.models import Record @@ -39,9 +39,58 @@ } +# ============================================================ +# 감정 번호별 색상 그룹 +# ============================================================ + +""" +감정별 기록의 도넛 그래프에서는 +감정 하나마다 다른 색을 사용하는 것이 아니라, + +비슷한 색상 계열의 감정을 +하나의 그래프 조각으로 묶어서 표시 + +예: +사랑, 짝사랑 +→ pink 그룹 + +기쁨, 만족, 신남, 행운, 맛있어! +→ yellow 그룹 + +중요: +여기서는 감정 이름이 아니라 +감정 번호를 기준으로 그룹을 지정 + +같은 이름의 감정이 여러 번호에 존재하더라도 +번호 기준으로 안정적으로 처리할 수 있음 +""" +EMOTION_COLOR_GROUP_MAP = { + 1: "blue", + 2: "pink", + 3: "purple", + 4: "purple", + 5: "yellow", + 6: "blue", + 7: "pink", + 8: "red", + 9: "orange", + 10: "blue", + 11: "red", + 12: "yellow", + 13: "blue", + 14: "orange", + 15: "yellow", + 16: "yellow", + 17: "red", + 18: "yellow", + 19: "purple", + 20: "purple", +} + + def get_emotion_name(emotion_number): """ - 감정 번호를 화면에 표시할 감정 이름으로 변환합니다. + 감정 번호를 화면에 표시할 감정 이름으로 변환 예: 1 → "슬픔" @@ -50,11 +99,14 @@ def get_emotion_name(emotion_number): try: # JSONField 값이나 폼 값이 문자열일 수도 있으므로 - # 비교하기 전에 정수로 변환합니다. - emotion_number = int(emotion_number) + # 비교하기 전에 정수로 변환 + emotion_number = int( + emotion_number, + ) except (TypeError, ValueError): - # 값이 없거나 숫자로 바꿀 수 없다면 빈 문자열을 반환합니다. + # 값이 없거나 숫자로 바꿀 수 없다면 + # 빈 문자열을 반환 return "" return EMOTION_NAME_MAP.get( @@ -63,36 +115,88 @@ def get_emotion_name(emotion_number): ) +def get_emotion_color_group(emotion_number): + """ + 감정 번호를 감정별 그래프에서 사용할 + 색상 그룹 이름으로 변환 + + 예: + 2 → pink + 11 → red + 16 → yellow + """ + + try: + emotion_number = int( + emotion_number, + ) + + except (TypeError, ValueError): + return "default" + + return EMOTION_COLOR_GROUP_MAP.get( + emotion_number, + "default", + ) + + @login_required def diary_list(request): """ 현재 로그인한 사용자가 작성한 실제 Record를 조회하여 - 전체 / 감정별 / 장소별 목록 페이지에 전달합니다. + 전체 / 감정별 / 장소별 목록 페이지에 전달 """ # ======================================================== # 1. 현재 선택한 탭과 필터값 # ======================================================== - selected_tab = request.GET.get("tab", "all") - selected_sort = request.GET.get("sort", "latest") + selected_tab = request.GET.get( + "tab", + "all", + ) - selected_start_date = request.GET.get("start_date", "") - selected_end_date = request.GET.get("end_date", "") + selected_sort = request.GET.get( + "sort", + "latest", + ) - if selected_tab not in {"all", "emotion", "location"}: + selected_start_date = request.GET.get( + "start_date", + "", + ) + + selected_end_date = request.GET.get( + "end_date", + "", + ) + + + # 허용하지 않는 탭 이름이 들어오면 + # 전체 기록 탭으로 돌려놓음 + if selected_tab not in { + "all", + "emotion", + "location", + }: selected_tab = "all" + # ======================================================== # 2. 현재 사용자의 실제 기록 조회 # ======================================================== - records_queryset = Record.objects.filter( - user=request.user, - ).select_related( - "place", + records_queryset = ( + Record.objects + .filter( + user=request.user, + ) + .select_related( + "place", + ) ) + # ======================================================== # 3. 시작일 필터 # ======================================================== @@ -109,8 +213,11 @@ def diary_list(request): ) except ValueError: + # 잘못된 날짜 값이 들어오면 + # 필터를 무시 selected_start_date = "" + # ======================================================== # 4. 종료일 필터 # ======================================================== @@ -129,11 +236,13 @@ def diary_list(request): except ValueError: selected_end_date = "" + # ======================================================== # 5. 정렬 # ======================================================== if selected_sort == "oldest": + records_queryset = records_queryset.order_by( "created_at", ) @@ -145,227 +254,506 @@ def diary_list(request): "-created_at", ) - records = list(records_queryset) + + # QuerySet을 리스트로 변환 + # + # 이후 여러 번 반복하면서 + # 감정/장소 데이터를 계산하기 때문에 + # 한 번 리스트로 만들어 사용 + records = list( + records_queryset, + ) + # ======================================================== - # 6. 각 기록에 화면용 감정 이름 추가 + # 6. 각 기록에 화면용 대표 감정 정보 추가 # ======================================================== for record in records: + + # 대표 감정 번호를 + # 사람이 읽을 수 있는 이름으로 바꿈 record.main_emotion_name = get_emotion_name( record.main_emotion, ) + # 지도나 다른 화면에서도 필요할 수 있으므로 + # 대표 감정의 색상 그룹도 record에 임시로 추가 + record.main_emotion_color_group = ( + get_emotion_color_group( + record.main_emotion, + ) + ) + + # ======================================================== - # 7. 감정별 기록 횟수 계산 + # 7. 대표 감정별 기록 횟수 계산 # ======================================================== - main_emotion_numbers = [ - int(record.main_emotion) - for record in records - if record.main_emotion - ] + """ + 여기서 중요한 점: + + record.emotions 전체를 사용하는 것이 아니라 + record.main_emotion만 사용 + + 따라서 사용자가 기록할 때 선택한 감정이 3개여도 + + 대표 감정 1개만 + 감정별 그래프 집계에 포함됨 + """ + + main_emotion_numbers = [] + + for record in records: + try: + emotion_number = int( + record.main_emotion, + ) + except (TypeError, ValueError): + continue + + main_emotion_numbers.append( + emotion_number, + ) + + + # 예: + # + # [16, 16, 12, 11] + # + # → + # + # { + # 16: 2, + # 12: 1, + # 11: 1 + # } emotion_counts = Counter( main_emotion_numbers, ) + + # 모든 감정 정보를 담는 목록 + # + # 기존 기능과의 호환을 위해 + # 기록 횟수가 0인 감정도 emotions에는 남겨둠 emotions = [] for emotion_id, emotion_name in EMOTION_NAME_MAP.items(): + record_count = emotion_counts.get( emotion_id, 0, ) - if record_count >= 5: - size = "large" - - elif record_count >= 2: - size = "medium" - - else: - size = "small" - emotions.append( { "id": emotion_id, + "name": emotion_name, + + # 해당 감정이 대표 감정으로 + # 기록된 횟수 "record_count": record_count, - "size": size, + + # 도넛 그래프에서 사용할 색상 그룹 + "color_group": ( + get_emotion_color_group( + emotion_id, + ) + ), } ) + + # ======================================================== + # 8. 실제로 기록된 대표 감정만 추리기 + # ======================================================== + + """ + 도넛 그래프에는 + 한 번도 기록되지 않은 감정은 나오면 안 됨 + + 따라서 record_count가 1 이상인 감정만 + recorded_emotions에 넣음 + """ + recorded_emotions = [ emotion for emotion in emotions if emotion["record_count"] > 0 ] - # 실제 기록된 감정이 있다면 + + # ======================================================== + # 9. 가장 많이 기록한 대표 감정 계산 + # ======================================================== + + """ + 기존 emotion_modal이나 다른 화면에서 + top_emotions를 사용할 수도 있으므로 + 이 데이터는 유지 + """ + if recorded_emotions: - # 가장 많은 기록 횟수를 구합니다. - # - # 예: - # 예민 1회, 행운 1회 - # → maximum_emotion_count = 1 + maximum_emotion_count = max( emotion["record_count"] for emotion in recorded_emotions ) - # 가장 많은 기록 횟수와 같은 감정을 - # 모두 공동 1위 목록에 넣습니다. - # - # 예: - # 예민 1회, 행운 1회 - # → [예민, 행운] top_emotions = [ emotion for emotion in recorded_emotions - if emotion["record_count"] == maximum_emotion_count + if ( + emotion["record_count"] + == maximum_emotion_count + ) ] else: - # 기록된 대표 감정이 없다면 빈 목록을 전달합니다. top_emotions = [] + # ======================================================== - # 8. 서울 지도용 장소 데이터 계산 + # 10. 도넛 그래프용 색상 그룹 데이터 생성 + # ======================================================== + + """ + 같은 색상 그룹에 속한 감정들을 하나로 합침 + + 예: + + 기쁨 2회 + 만족 1회 + 신남 1회 + + 모두 yellow 그룹이라면 + + yellow: + count = 4 + emotions = ["만족", "신남", "기쁨"] + + 가 됩니다. + + JavaScript는 이 데이터를 사용하여 + 원 그래프 조각 크기를 계산할 수 있음 + """ + + emotion_chart_group_dict = {} + + + for emotion in recorded_emotions: + + color_group = emotion[ + "color_group" + ] + + emotion_name = emotion[ + "name" + ] + + record_count = emotion[ + "record_count" + ] + + + # 해당 색상 그룹이 처음 등장하면 + # 기본 구조를 생성 + if color_group not in emotion_chart_group_dict: + + emotion_chart_group_dict[color_group] = { + "color_group": color_group, + + # 이 색상 그룹에 속한 + # 대표 감정 기록 수의 총합 + "record_count": 0, + + # hover 시 보여줄 감정 이름 목록 + "emotion_names": [], + } + + + # 색상 그룹의 전체 기록 수에 + # 현재 감정 기록 수를 더함 + emotion_chart_group_dict[ + color_group + ]["record_count"] += record_count + + + # 같은 이름을 중복해서 + # 툴팁에 표시하지 않도록 함 + # + # 예: + # 감정 번호 1, 6, 10이 모두 "슬픔"이어도 + # hover에는 "슬픔" 한 번만 표시함 + if ( + emotion_name + not in emotion_chart_group_dict[ + color_group + ]["emotion_names"] + ): + emotion_chart_group_dict[ + color_group + ]["emotion_names"].append( + emotion_name, + ) + + + # 딕셔너리를 템플릿에서 사용하기 쉬운 + # 리스트 형태로 바꿈 + emotion_chart_groups = list( + emotion_chart_group_dict.values() + ) + + + # ======================================================== + # 11. 전체 대표 감정 기록 수 + # ======================================================== + + """ + 현재 Record 하나당 대표 감정이 하나이므로 + 정상적인 데이터라면 records 개수와 같음 + + 그래프 가운데의 + + 전체 기록 + 8개 + + 를 표시할 때 사용할 값 + """ + + total_record_count = sum( + emotion["record_count"] + for emotion in recorded_emotions + ) + + + # ======================================================== + # 12. 서울 지도용 장소 데이터 계산 # ======================================================== district_records = {} + for record in records: + # Record에 연결된 Place가 없으면 - # 지도에 표시할 구 정보를 알 수 없으므로 제외합니다. + # 지도에 표시할 수 없으므로 제외 if not record.place: continue + # Place 모델의 district에는 - # "성북구", "강남구"처럼 서울 자치구 이름이 저장됩니다. + # "성북구", "강남구"와 같은 + # 서울 자치구 이름이 저장됨 district_name = ( record.place.district.strip() if record.place.district else "" ) - # district 값이 비어 있다면 제외합니다. + + # 구 이름이 없다면 제외 if not district_name: continue - emotion_name = record.main_emotion_name - # 대표 감정 정보가 없다면 - # 감정 색상을 계산할 수 없으므로 제외합니다. + emotion_name = ( + record.main_emotion_name + ) + + + # 대표 감정 정보가 없으면 + # 지도 감정 색상을 정할 수 없으므로 제외 if not emotion_name: continue - # 해당 구가 처음 등장했다면 - # 기록을 담을 빈 배열을 만듭니다. + if district_name not in district_records: - district_records[district_name] = [] - district_records[district_name].append( + district_records[ + district_name + ] = [] + + + district_records[ + district_name + ].append( { "emotion_name": emotion_name, - "created_at": record.created_at, + + "created_at": ( + record.created_at + ), } ) district_map_data = [] - for district_name, district_record_list in district_records.items(): - # 해당 구에서 기록된 대표 감정들의 이름 + + for ( + district_name, + district_record_list, + ) in district_records.items(): + + # 해당 구에서 기록된 + # 대표 감정 이름 목록 emotion_names = [ item["emotion_name"] for item in district_record_list ] - # 해당 구에서 가장 많이 기록된 감정 + + # 가장 많이 기록된 감정 계산 emotion_name_counts = Counter( emotion_names, ) - dominant_emotion = emotion_name_counts.most_common(1)[0][0] + dominant_emotion = ( + emotion_name_counts + .most_common(1)[0][0] + ) + - # 해당 구의 가장 최근 기록 + # 가장 최근 기록 계산 latest_record = max( district_record_list, - key=lambda item: item["created_at"], + key=lambda item: item[ + "created_at" + ], ) + district_map_data.append( { # SVG path의 id와 비교할 구 이름 - # 예: "성북구" "district_name": district_name, # 해당 구에서 작성한 기록 수 - "visit_count": len(district_record_list), + "visit_count": len( + district_record_list, + ), # 해당 구에서 가장 많이 기록된 감정 - "dominant_emotion": dominant_emotion, + "dominant_emotion": ( + dominant_emotion + ), - # 해당 구에서 가장 최근에 기록된 감정 - "latest_emotion": latest_record["emotion_name"], + # 해당 구에서 가장 최근 대표 감정 + "latest_emotion": ( + latest_record[ + "emotion_name" + ] + ), } ) + # ======================================================== - # 9. 장소별 기록 횟수 계산 + # 13. 장소별 기록 횟수 계산 # ======================================================== location_counts = Counter( record.place.district for record in records - if record.place and record.place.district + if ( + record.place + and record.place.district + ) ) + locations = [ { "name": place_name, "record_count": count, } - for place_name, count in location_counts.items() + for ( + place_name, + count, + ) in location_counts.items() ] + locations.sort( - key=lambda location: location["record_count"], + key=lambda location: ( + location["record_count"] + ), reverse=True, ) + # ======================================================== - # 10. 템플릿에 데이터 전달 + # 14. 템플릿에 데이터 전달 # ======================================================== context = { + # ---------------------------------------------------- + # 공통 기록 + # ---------------------------------------------------- "records": records, + + + # ---------------------------------------------------- + # 감정별 기록 + # ---------------------------------------------------- + + # 모든 감정 "emotions": emotions, - # 실제 기록 횟수가 1회 이상인 대표 감정만 전달합니다. + # 실제 대표 감정으로 기록된 감정만 "recorded_emotions": recorded_emotions, + # 가장 많이 기록한 대표 감정 "top_emotions": top_emotions, + + # 도넛 그래프용 색상 그룹 데이터 + "emotion_chart_groups": emotion_chart_groups, + + # 도넛 그래프 가운데 표시할 전체 기록 수 + "total_record_count": total_record_count, + + + # ---------------------------------------------------- + # 장소별 기록 + # ---------------------------------------------------- "locations": locations, - "district_map_data": district_map_data, + "district_map_data": ( + district_map_data + ), + + + # ---------------------------------------------------- + # 현재 탭 및 필터 상태 + # ---------------------------------------------------- "selected_tab": selected_tab, + "selected_sort": selected_sort, - "selected_start_date": selected_start_date, - "selected_end_date": selected_end_date, + + "selected_start_date": ( + selected_start_date + ), + + "selected_end_date": ( + selected_end_date + ), "selected_emotion": request.GET.get( "emotion", "", ), + "selected_location": request.GET.get( "location", "", ), } + return render( request, "diary/list.html", @@ -377,53 +765,55 @@ def diary_list(request): def emotion_calendar(request): """ 현재 로그인한 사용자의 기록을 날짜별로 정리하여 - 감정 캘린더 페이지에 전달합니다. + 감정 캘린더 페이지에 전달 같은 날짜에 기록이 여러 개 있다면 - 가장 최근에 작성한 기록의 대표 감정을 사용합니다. + 가장 최근에 작성한 기록의 대표 감정을 사용 """ - # 오늘 날짜를 기준으로 캘린더의 최초 연도와 월을 설정합니다. + # 오늘 날짜를 기준으로 + # 캘린더 최초 연도와 월을 설정 today = timezone.localdate() - # 최신 기록부터 조회합니다. + + # 최신 기록부터 조회 # - # 아래에서 날짜별 데이터에 처음 들어간 기록만 사용하므로 - # 같은 날짜에 여러 기록이 있으면 가장 최신 기록이 남습니다. + # 아래에서 날짜별 데이터에 + # 처음 들어간 기록만 사용하므로 + # 같은 날짜에 여러 기록이 있으면 + # 가장 최신 기록이 남음 records = Record.objects.filter( user=request.user, ).order_by( "-created_at", ) - # JavaScript에 전달할 날짜별 감정 데이터입니다. - # - # 결과 예: - # { - # "2026-08-01": { - # "number": 7, - # "name": "짝사랑", - # "image_name": "emotion-07.png" - # } - # } + + # JavaScript에 전달할 + # 날짜별 감정 데이터 calendar_emotions = {} + for record in records: - # created_at이 시간대 정보를 가진 경우 - # 한국 시간 기준 날짜로 변환합니다. + + # created_at을 + # 한국 시간 기준 날짜로 변환 created_date = timezone.localtime( record.created_at, ).date() + date_key = created_date.strftime( "%Y-%m-%d", ) - # 이미 같은 날짜의 최신 기록이 저장되어 있다면 - # 그보다 오래된 기록은 건너뜁니다. + + # 이미 같은 날짜의 최신 기록이 있다면 + # 오래된 기록은 건너뜀 if date_key in calendar_emotions: continue + try: emotion_number = int( record.main_emotion, @@ -432,95 +822,106 @@ def emotion_calendar(request): except (TypeError, ValueError): continue - calendar_emotions[date_key] = { - # 감정 이미지 번호 + + calendar_emotions[ + date_key + ] = { + # 감정 번호 "number": emotion_number, - # 이미지의 대체 텍스트와 툴팁에 사용할 감정 이름 + # 감정 이름 "name": get_emotion_name( emotion_number, ), - # 실제 records 이미지 폴더의 파일명 + # 감정 이미지 파일명 "image_name": ( f"emotion-{emotion_number:02d}.png" ), - # 날짜 칸이나 감정 이미지를 눌렀을 때 - # 해당 기록 상세 페이지로 이동할 때 사용할 ID + # 상세 페이지 이동용 기록 ID "record_id": record.pk, } + context = { - # 최초 화면에 표시할 연도와 월 + # 최초 화면에 표시할 연도 "calendar_year": today.year, + + # 최초 화면에 표시할 월 "calendar_month": today.month, - # calendar.html의 json_script를 통해 - # JavaScript에 안전하게 전달됩니다. - "calendar_emotions": calendar_emotions, + # 날짜별 대표 감정 데이터 + "calendar_emotions": ( + calendar_emotions + ), } + return render( request, "diary/calendar.html", context, ) + @login_required def diary_detail(request, pk): """ 현재 로그인한 사용자가 작성한 기록 하나를 조회하여 - 다이어리 상세 페이지에 전달합니다. + 다이어리 상세 페이지에 전달 - pk는 기록의 고유 번호입니다. + pk는 기록의 고유 번호 예: /diary/2/ - → id가 2인 Record를 조회합니다. + → id가 2인 Record를 조회 """ - # 다른 사용자의 기록을 주소로 직접 접근하지 못하도록 - # user=request.user 조건을 함께 사용합니다. + # 다른 사용자의 기록을 + # 주소로 직접 접근하지 못하도록 + # user=request.user 조건을 함께 사용 record = get_object_or_404( Record, pk=pk, user=request.user, ) - # 대표 감정 번호 - # 대표 감정 정보입니다. - # - # Record.main_emotion에는 1~20 사이 번호가 저장됩니다. + + # ======================================================== + # 대표 감정 + # ======================================================== + main_emotion_number = int( record.main_emotion, ) + main_emotion = { + # 감정 번호 "number": main_emotion_number, - # diary/views.py 위쪽에 이미 있는 - # get_emotion_name() 함수를 사용합니다. + # 화면에 표시할 감정 이름 "name": get_emotion_name( main_emotion_number, ), - # 실제 감정 이미지 파일명입니다. - # - # 예: - # 1 → emotion-01.png - # 7 → emotion-07.png - # 20 → emotion-20.png + # 실제 감정 이미지 파일명 "image_name": ( f"emotion-{main_emotion_number:02d}.png" ), } - # 대표 감정을 제외한 나머지 감정들을 - # 보조 감정 목록으로 만듭니다. + + # ======================================================== + # 보조 감정 + # ======================================================== + secondary_emotions = [] + for emotion_number in record.emotions: + try: emotion_number = int( emotion_number, @@ -529,39 +930,47 @@ def diary_detail(request, pk): except (TypeError, ValueError): continue - # 대표 감정은 가운데 카드에서 따로 표시하므로 - # 보조 감정 목록에서는 제외합니다. - if emotion_number == main_emotion_number: + + # 대표 감정은 가운데에서 따로 표시하므로 + # 보조 감정 목록에서는 제외 + if ( + emotion_number + == main_emotion_number + ): continue + secondary_emotions.append( { "number": emotion_number, + "name": get_emotion_name( emotion_number, ), + "image_name": ( f"emotion-{emotion_number:02d}.png" ), } ) - # 감정은 최대 3개이므로 보조 감정은 최대 2개입니다. - # - # 첫 번째 보조 감정은 왼쪽, - # 두 번째 보조 감정은 오른쪽에 표시합니다. + + # 감정은 최대 3개이므로 + # 대표 감정 제외 후 보조 감정은 최대 2개 left_emotion = ( secondary_emotions[0] if len(secondary_emotions) >= 1 else None ) + right_emotion = ( secondary_emotions[1] if len(secondary_emotions) >= 2 else None ) + context = { # 기록 전체 정보 "record": record, @@ -576,6 +985,7 @@ def diary_detail(request, pk): "right_emotion": right_emotion, } + return render( request, "diary/detail.html", From c72d36fe459e160548efb4900f94c4fbe896edf0 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 12:02:07 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=EB=8C=80=ED=91=9C=20=EA=B0=90?= =?UTF-8?q?=EC=A0=95=20=EB=8F=84=EB=84=9B=20=EA=B7=B8=EB=9E=98=ED=94=84=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emotion_list.html 내부 전체 감정 개수 코드 변경 --- diary/static/diary/js/diary.js | 1204 +++++++++++------ .../diary/partials/emotion_list.html | 24 +- 2 files changed, 812 insertions(+), 416 deletions(-) diff --git a/diary/static/diary/js/diary.js b/diary/static/diary/js/diary.js index 2808cd0..884db7d 100644 --- a/diary/static/diary/js/diary.js +++ b/diary/static/diary/js/diary.js @@ -3,7 +3,7 @@ document.addEventListener("DOMContentLoaded", () => { /* ========================================================= - 1. 지도 감정 색상 정보 + 1. 감정 색상 정보 ========================================================= */ const emotionColors = { @@ -34,8 +34,21 @@ document.addEventListener("DOMContentLoaded", () => { /* - 지도에서 '구별로 다른 색' 모드를 선택했을 때 - 각 구에 순서대로 적용할 색상 목록입니다. + 감정별 도넛 그래프 색상 + */ + const emotionChartColors = { + red: "#ef9a9a", + blue: "#9fc5e8", + yellow: "#f8dc7d", + pink: "#efb2c7", + orange: "#f9c25c", + purple: "#b3b2db", + default: "#d4d4d4", + }; + + + /* + 구별 색상 모드용 색상 목록 */ const districtColors = [ "#f3b7b7", @@ -60,38 +73,15 @@ document.addEventListener("DOMContentLoaded", () => { 2. 지도 설정 요소 ========================================================= */ - /* - 지도 오른쪽 위의 점 세 개 버튼입니다. - */ const mapSettingsButton = document.querySelector("#map-settings-button"); - - /* - 점 세 개 버튼을 눌렀을 때 열리는 - 지도 색상 설정 메뉴입니다. - */ const mapSettingsMenu = document.querySelector("#map-settings-menu"); - - /* - 지도 색상 방식을 선택하는 버튼들입니다. - - 예: - - 같은 색으로 통일 - - 가장 많은 감정 색 - - 가장 최근 감정 색 - - 방문 횟수에 따른 농도 - - 구별로 다른 색 - */ const mapModeButtons = document.querySelectorAll("[data-map-mode]"); - - /* - Django가 HTML에 넣어둔 구별 기록 데이터입니다. - */ const districtDataElements = document.querySelectorAll( "#district-map-data [data-district]" @@ -127,7 +117,10 @@ document.addEventListener("DOMContentLoaded", () => { function getDistrictMapData() { return Array.from(districtDataElements).map((element) => ({ district: element.dataset.district.trim(), - count: Number(element.dataset.count), + + count: Number( + element.dataset.count + ), dominantEmotion: element.dataset.dominantEmotion.trim(), @@ -147,15 +140,10 @@ document.addEventListener("DOMContentLoaded", () => { .querySelectorAll(".seoul-map path") .forEach((path) => { - /* - JavaScript로 직접 넣었던 fill 색상을 제거합니다. - */ + // JavaScript fill 제거 path.style.fill = ""; - - /* - 기록이 있는 구에 붙었던 클래스를 제거합니다. - */ + // 기록 표시 클래스 제거 path.classList.remove("has-record"); }); } @@ -167,46 +155,34 @@ document.addEventListener("DOMContentLoaded", () => { function applyMapMode(mode) { - /* - 새로운 색상을 적용하기 전에 - 이전 지도 색상을 먼저 초기화합니다. - */ + // 기존 지도 색상 초기화 clearDistrictStyles(); - /* - HTML에 담긴 구별 데이터를 배열로 가져옵니다. - */ - const mapData = getDistrictMapData(); - + // 구별 기록 데이터 가져오기 + const mapData = + getDistrictMapData(); - /* - 방문 횟수 농도를 계산하기 위해 - 가장 방문 횟수가 많은 구의 기록 수를 구합니다. - 기록이 없을 때 0으로 나누는 문제를 막기 위해 - 최소값을 1로 설정합니다. - */ + // 최대 방문 횟수 계산 const maximumCount = Math.max( - ...mapData.map((district) => district.count), + ...mapData.map( + (district) => district.count + ), 1 ); mapData.forEach((district, index) => { - /* - district.district와 같은 id를 가진 - 서울 지도 SVG path를 찾습니다. - - 예: - district.district가 "성북구"라면 - id="성북구"인 path를 찾습니다. - */ + // 구 이름과 같은 SVG path 탐색 const path = - document.getElementById(district.district); + document.getElementById( + district.district + ); + // SVG path가 없는 경우 제외 if (!path) { console.warn( `SVG에서 구를 찾지 못했습니다: ${district.district}` @@ -216,74 +192,67 @@ document.addEventListener("DOMContentLoaded", () => { } - /* - 기록이 있는 구라는 표시를 추가합니다. - */ + // 기록 있는 구 표시 path.classList.add("has-record"); - /* - 모든 기록된 구를 같은 색으로 표시합니다. - */ + // 동일 색상 모드 if (mode === "uniform") { path.style.fill = "#dce9b7"; } - /* - 해당 구에서 가장 많이 기록된 감정의 색상을 사용합니다. - */ + // 최다 감정 색상 모드 if (mode === "dominant") { path.style.fill = - emotionColors[district.dominantEmotion] + emotionColors[ + district.dominantEmotion + ] || "#d4d4d4"; } - /* - 해당 구에서 가장 최근에 기록한 감정의 색상을 사용합니다. - */ + // 최근 감정 색상 모드 if (mode === "latest") { path.style.fill = - emotionColors[district.latestEmotion] + emotionColors[ + district.latestEmotion + ] || "#d4d4d4"; } - /* - 방문 횟수가 많을수록 색을 진하게 표시합니다. - */ + // 방문 횟수 농도 모드 if (mode === "intensity") { + const minimumOpacity = 0.05; const ratio = - district.count / maximumCount; + district.count + / maximumCount; const opacity = minimumOpacity - + ratio * (1 - minimumOpacity); + + ratio + * (1 - minimumOpacity); path.style.fill = `rgba(245, 196, 80, ${opacity})`; } - /* - 각 구에 서로 다른 색상을 순서대로 적용합니다. - */ + // 구별 다른 색상 모드 if (mode === "rainbow") { path.style.fill = districtColors[ - index % districtColors.length + index + % districtColors.length ]; } }); - /* - 현재 선택한 지도 색상 방식 버튼에만 - active 클래스를 추가합니다. - */ + // 선택 버튼 active 처리 mapModeButtons.forEach((button) => { button.classList.toggle( "active", @@ -292,11 +261,11 @@ document.addEventListener("DOMContentLoaded", () => { }); - /* - 새로고침한 뒤에도 선택한 지도 색상 방식이 - 유지되도록 브라우저 저장소에 저장합니다. - */ - localStorage.setItem("diaryMapMode", mode); + // 선택 모드 저장 + localStorage.setItem( + "diaryMapMode", + mode + ); } @@ -306,31 +275,26 @@ document.addEventListener("DOMContentLoaded", () => { function openLocationModal(locationName) { - /* - 장소 팝업 요소가 없는 페이지에서는 - 아래 코드를 실행하지 않습니다. - */ - if (!locationModal || !locationModalName) { + // 팝업 요소 없는 경우 종료 + if ( + !locationModal + || !locationModalName + ) { return; } - /* - 선택한 장소와 일치하는 기록의 개수입니다. - */ let visibleRecordCount = 0; - /* - 팝업 제목에 선택한 구 이름을 넣습니다. - */ - locationModalName.textContent = locationName; + // 팝업 장소명 설정 + locationModalName.textContent = + locationName; - /* - 해당 구에 작성된 기록만 표시합니다. - */ + // 선택한 장소 기록만 표시 locationModalRecords.forEach((record) => { + const recordLocation = record.dataset.recordLocation.trim(); @@ -345,26 +309,21 @@ document.addEventListener("DOMContentLoaded", () => { }); - /* - 일치하는 기록이 없다면 - 빈 기록 안내 문구를 표시합니다. - */ + // 기록 없음 문구 처리 if (locationModalEmpty) { locationModalEmpty.hidden = visibleRecordCount !== 0; } - /* - 장소별 기록 팝업을 엽니다. - */ + // 팝업 표시 locationModal.hidden = false; - /* - 팝업이 열린 동안 뒤쪽 페이지 스크롤을 막습니다. - */ - document.body.classList.add("modal-open"); + // 배경 스크롤 차단 + document.body.classList.add( + "modal-open" + ); } @@ -373,28 +332,40 @@ document.addEventListener("DOMContentLoaded", () => { ========================================================= */ function closeLocationModal() { + + // 팝업 없는 경우 종료 if (!locationModal) { return; } + + // 팝업 숨김 locationModal.hidden = true; - document.body.classList.remove("modal-open"); + + // 배경 스크롤 복구 + document.body.classList.remove( + "modal-open" + ); } /* ========================================================= - 9. 기록이 있는 서울 구역 클릭 이벤트 + 9. 기록 있는 서울 구역 클릭 ========================================================= */ districtDataElements.forEach((element) => { + const districtName = element.dataset.district.trim(); const districtPath = - document.getElementById(districtName); + document.getElementById( + districtName + ); + // SVG path 없는 경우 제외 if (!districtPath) { console.warn( `클릭 이벤트를 연결할 구를 찾지 못했습니다: ${districtName}` @@ -404,97 +375,92 @@ document.addEventListener("DOMContentLoaded", () => { } - /* - tabindex는 넣지 않습니다. - - SVG를 클릭했을 때 직사각형 포커스 테두리가 - 나타나는 문제를 방지합니다. - */ - districtPath.addEventListener("click", () => { - openLocationModal(districtName); - }); + // 장소 팝업 열기 + districtPath.addEventListener( + "click", + () => { + openLocationModal( + districtName + ); + } + ); }); /* ========================================================= - 10. 장소별 기록 팝업 닫기 버튼 + 10. 장소 팝업 닫기 ========================================================= */ - locationModalCloseButtons.forEach((button) => { - button.addEventListener( - "click", - closeLocationModal - ); - }); + locationModalCloseButtons.forEach( + (button) => { + + button.addEventListener( + "click", + closeLocationModal + ); + } + ); /* ========================================================= - 11. 지도 점 세 개 설정 메뉴 + 11. 지도 설정 메뉴 ========================================================= */ - if (mapSettingsButton && mapSettingsMenu) { + if ( + mapSettingsButton + && mapSettingsMenu + ) { - /* - 점 세 개 버튼을 누르면 - 지도 설정 메뉴를 열거나 닫습니다. - */ - mapSettingsButton.addEventListener("click", (event) => { + // 점 세 개 버튼 클릭 + mapSettingsButton.addEventListener( + "click", + (event) => { - /* - 버튼 클릭이 document 클릭 이벤트까지 - 전달되는 것을 막습니다. + // document 클릭 이벤트 전달 차단 + event.stopPropagation(); - 이 코드가 없으면 메뉴를 열자마자 - 아래 document 클릭 이벤트가 실행되어 - 다시 닫힐 수 있습니다. - */ - event.stopPropagation(); + // 변경 후 열림 상태 계산 + const willOpen = + mapSettingsMenu.hidden; - /* - 현재 메뉴가 숨겨져 있다면 true입니다. - */ - const willOpen = mapSettingsMenu.hidden; + // 메뉴 열기/닫기 + mapSettingsMenu.hidden = + !willOpen; - /* - 숨겨져 있었다면 열고, - 열려 있었다면 닫습니다. - */ - mapSettingsMenu.hidden = !willOpen; + // 접근성 상태 변경 + mapSettingsButton.setAttribute( + "aria-expanded", + String(willOpen) + ); + } + ); - /* - 접근성 상태도 메뉴 상태와 맞춰 변경합니다. - */ - mapSettingsButton.setAttribute( - "aria-expanded", - String(willOpen) - ); - }); + // 메뉴 내부 클릭 전파 차단 + mapSettingsMenu.addEventListener( + "click", + (event) => { + event.stopPropagation(); + } + ); - /* - 설정 메뉴 내부를 클릭했을 때는 - document 클릭 이벤트로 전달되지 않게 합니다. - */ - mapSettingsMenu.addEventListener("click", (event) => { - event.stopPropagation(); - }); + // 메뉴 바깥 클릭 시 닫기 + document.addEventListener( + "click", + () => { - /* - 지도 설정 메뉴 바깥을 클릭하면 - 설정 메뉴를 닫습니다. - */ - document.addEventListener("click", () => { - mapSettingsMenu.hidden = true; + mapSettingsMenu.hidden = true; - mapSettingsButton.setAttribute( - "aria-expanded", - "false" - ); - }); + mapSettingsButton.setAttribute( + "aria-expanded", + "false" + ); + } + ); } @@ -503,92 +469,97 @@ document.addEventListener("DOMContentLoaded", () => { ========================================================= */ mapModeButtons.forEach((button) => { - button.addEventListener("click", () => { - /* - 클릭한 버튼의 data-map-mode 값을 가져옵니다. - */ - const selectedMode = - button.dataset.mapMode; + button.addEventListener( + "click", + () => { + // 선택 모드 가져오기 + const selectedMode = + button.dataset.mapMode; - /* - 선택한 방식으로 지도 색상을 변경합니다. - */ - applyMapMode(selectedMode); + // 지도 색상 변경 + applyMapMode( + selectedMode + ); - /* - 색상 방식 선택 후 설정 메뉴를 닫습니다. - */ - if (mapSettingsMenu) { - mapSettingsMenu.hidden = true; - } - if (mapSettingsButton) { - mapSettingsButton.setAttribute( - "aria-expanded", - "false" - ); + // 설정 메뉴 닫기 + if (mapSettingsMenu) { + mapSettingsMenu.hidden = true; + } + + + // 접근성 상태 변경 + if (mapSettingsButton) { + mapSettingsButton.setAttribute( + "aria-expanded", + "false" + ); + } } - }); + ); }); /* ========================================================= - 13. 페이지 진입 시 저장된 지도 색상 방식 적용 + 13. 저장된 지도 색상 적용 ========================================================= */ if (districtDataElements.length > 0) { + const savedMapMode = - localStorage.getItem("diaryMapMode") + localStorage.getItem( + "diaryMapMode" + ) || "dominant"; - applyMapMode(savedMapMode); + + applyMapMode( + savedMapMode + ); } /* ========================================================= - 14. 전체 기록 정렬 및 기간 설정 요소 + 14. 전체 기록 필터 요소 ========================================================= */ - /* - 최신순·과거순 선택창입니다. - */ const sortSelect = document.querySelector("#diary-sort"); - - /* - 최신순과 기간 설정을 포함하는 전체 폼입니다. - */ const filterForm = - document.querySelector("#diary-filter-form"); - + document.querySelector( + "#diary-filter-form" + ); - /* - 기간 설정창을 여는 버튼입니다. - */ const periodToggleButton = - document.querySelector("#period-toggle-button"); - + document.querySelector( + "#period-toggle-button" + ); - /* - 시작일·종료일·적용·초기화가 들어 있는 - 실제 기간 설정창입니다. - */ const periodFilter = - document.querySelector("#period-filter"); + document.querySelector( + "#period-filter" + ); /* ========================================================= - 15. 최신순·과거순 선택 시 자동 제출 + 15. 최신순·과거순 변경 ========================================================= */ - if (sortSelect && filterForm) { - sortSelect.addEventListener("change", () => { - filterForm.submit(); - }); + if ( + sortSelect + && filterForm + ) { + + sortSelect.addEventListener( + "change", + () => { + filterForm.submit(); + } + ); } @@ -598,34 +569,26 @@ document.addEventListener("DOMContentLoaded", () => { function openPeriodFilter() { - /* - 현재 페이지에 기간 설정 요소가 없다면 - 함수를 종료합니다. - */ - if (!periodToggleButton || !periodFilter) { + // 필수 요소 없는 경우 종료 + if ( + !periodToggleButton + || !periodFilter + ) { return; } - /* - hidden 속성을 제거해 기간 설정창을 표시합니다. - */ + // 기간 설정창 표시 periodFilter.hidden = false; - /* - 기간 설정 버튼에 active 클래스를 추가합니다. - - CSS의 - .period-toggle-button.active - 스타일이 적용됩니다. - */ - periodToggleButton.classList.add("active"); + // 버튼 활성화 + periodToggleButton.classList.add( + "active" + ); - /* - 스크린 리더에 설정창이 열렸다는 것을 전달합니다. - */ + // 접근성 상태 변경 periodToggleButton.setAttribute( "aria-expanded", "true" @@ -639,30 +602,26 @@ document.addEventListener("DOMContentLoaded", () => { function closePeriodFilter() { - /* - 현재 페이지에 기간 설정 요소가 없다면 - 함수를 종료합니다. - */ - if (!periodToggleButton || !periodFilter) { + // 필수 요소 없는 경우 종료 + if ( + !periodToggleButton + || !periodFilter + ) { return; } - /* - hidden 속성을 추가해 기간 설정창을 숨깁니다. - */ + // 기간 설정창 숨김 periodFilter.hidden = true; - /* - 버튼의 활성화 스타일을 제거합니다. - */ - periodToggleButton.classList.remove("active"); + // 버튼 활성화 제거 + periodToggleButton.classList.remove( + "active" + ); - /* - 스크린 리더에 설정창이 닫혔다는 것을 전달합니다. - */ + // 접근성 상태 변경 periodToggleButton.setAttribute( "aria-expanded", "false" @@ -674,115 +633,548 @@ document.addEventListener("DOMContentLoaded", () => { 18. 기간 설정 버튼 클릭 ========================================================= */ - if (periodToggleButton && periodFilter) { - periodToggleButton.addEventListener("click", () => { + if ( + periodToggleButton + && periodFilter + ) { - /* - 버튼을 누르기 전에 기간 설정창이 - 숨겨져 있었는지 확인합니다. - - true - → 현재 닫혀 있으므로 열어야 합니다. + periodToggleButton.addEventListener( + "click", + () => { - false - → 현재 열려 있으므로 닫아야 합니다. - */ - const shouldOpen = periodFilter.hidden; + const shouldOpen = + periodFilter.hidden; - if (shouldOpen) { - openPeriodFilter(); - } else { - closePeriodFilter(); + if (shouldOpen) { + openPeriodFilter(); + } else { + closePeriodFilter(); + } } - }); + ); /* ===================================================== - 19. 기간 설정창 바깥 클릭 시 닫기 + 19. 기간 설정창 바깥 클릭 ===================================================== */ - document.addEventListener("click", (event) => { + document.addEventListener( + "click", + (event) => { + + const clickedElement = + event.target; + + + // 기간 설정 버튼 클릭 여부 + const clickedToggleButton = + periodToggleButton.contains( + clickedElement + ); + + + // 기간 설정창 내부 클릭 여부 + const clickedInsidePeriodFilter = + periodFilter.contains( + clickedElement + ); + + + // 외부 클릭 시 닫기 + if ( + !clickedToggleButton + && !clickedInsidePeriodFilter + ) { + closePeriodFilter(); + } + } + ); + } + + + /* ========================================================= + 20. 감정 도넛 그래프 요소 + ========================================================= */ + + const emotionChart = + document.querySelector( + "#emotion-chart" + ); + + const emotionChartSegments = + document.querySelector( + "#emotion-chart-segments" + ); + + const emotionChartWrapper = + document.querySelector( + "#emotion-chart-wrapper" + ); + + const emotionChartTooltip = + document.querySelector( + "#emotion-chart-tooltip" + ); + + const emotionChartDataItems = + document.querySelectorAll( + "#emotion-chart-data .emotion-chart-data-item" + ); + + + /* ========================================================= + 21. 감정 데이터를 색상 그룹별로 합치기 + ========================================================= */ + + function getEmotionChartGroups() { + + // 색상 그룹별 임시 저장 객체 + const groupedData = {}; + + + emotionChartDataItems.forEach( + (element) => { + + // 감정 이름 + const emotionName = + element.dataset.emotionName.trim(); + + + // 대표 감정 기록 횟수 + const emotionCount = + Number( + element.dataset.emotionCount + ); + + + // 색상 그룹 이름 + const colorGroup = + element.dataset.emotionColorGroup.trim(); + + + // 잘못된 데이터 제외 + if ( + !emotionName + || emotionCount <= 0 + ) { + return; + } + + + // 최초 색상 그룹 생성 + if (!groupedData[colorGroup]) { + + groupedData[colorGroup] = { + colorGroup: colorGroup, + count: 0, + emotionNames: [], + }; + } + + + // 기록 횟수 합산 + groupedData[colorGroup].count += + emotionCount; + + + // 감정 이름 중복 제거 후 추가 + if ( + !groupedData[ + colorGroup + ].emotionNames.includes( + emotionName + ) + ) { + groupedData[ + colorGroup + ].emotionNames.push( + emotionName + ); + } + } + ); + + + // 객체를 배열로 변환 + return Object.values( + groupedData + ); + } + + + /* ========================================================= + 22. 감정 그래프 툴팁 표시 + ========================================================= */ + + function showEmotionChartTooltip( + emotionNames, + event + ) { + + // 필수 요소 없는 경우 종료 + if ( + !emotionChartTooltip + || !emotionChartWrapper + ) { + return; + } + + + // 감정 이름만 표시 + emotionChartTooltip.textContent = + emotionNames.join(" · "); + + + // 툴팁 표시 + emotionChartTooltip.hidden = false; + + + // wrapper 위치 계산 + const wrapperRect = + emotionChartWrapper.getBoundingClientRect(); + + + // 마우스 기준 툴팁 위치 + const tooltipX = + event.clientX + - wrapperRect.left; + + const tooltipY = + event.clientY + - wrapperRect.top; + + + // 마우스보다 약간 위쪽에 배치 + emotionChartTooltip.style.left = + `${tooltipX}px`; + + emotionChartTooltip.style.top = + `${tooltipY - 16}px`; + } + + + /* ========================================================= + 23. 감정 그래프 툴팁 숨기기 + ========================================================= */ + + function hideEmotionChartTooltip() { + + // 툴팁 없는 경우 종료 + if (!emotionChartTooltip) { + return; + } + + + // 툴팁 숨김 + emotionChartTooltip.hidden = true; + } + + + /* ========================================================= + 24. 감정 SVG 도넛 그래프 생성 + ========================================================= */ + + function renderEmotionChart() { + + // 그래프 요소 없는 경우 종료 + if ( + !emotionChart + || !emotionChartSegments + ) { + return; + } + + + // 기존 그래프 조각 제거 + emotionChartSegments.innerHTML = ""; + + + // 색상 그룹별 데이터 생성 + const chartGroups = + getEmotionChartGroups(); + + + // 기록 없는 경우 종료 + if (chartGroups.length === 0) { + return; + } + + + // 전체 대표 감정 기록 수 계산 + const totalCount = + chartGroups.reduce( + (sum, group) => + sum + group.count, + 0 + ); + + + // 전체 기록 없는 경우 종료 + if (totalCount <= 0) { + return; + } + + + /* + SVG 원 정보 + + emotion_list.html: + cx = 160 + cy = 160 + r = 110 + */ + const centerX = 160; + const centerY = 160; + const radius = 110; + + + // 원 둘레 계산 + const circumference = + 2 * Math.PI * radius; + + + /* + 조각 사이 간격 + + 값 증가 + → 조각 사이 간격 증가 + + 값 감소 + → 조각 사이 간격 감소 + */ + const segmentGap = 7; + + + /* + 시작 위치 + + SVG 원은 기본적으로 오른쪽에서 시작함. + + -circumference / 4 적용 + → 그래프 시작점을 12시 방향으로 이동 + */ + let currentOffset = + -circumference / 4; + + + chartGroups.forEach((group) => { + + // 그룹 비율 계산 + const ratio = + group.count / totalCount; + + + // 그룹이 차지하는 원 둘레 길이 + const segmentLength = + circumference * ratio; + /* - 실제로 클릭된 HTML 요소입니다. + 실제 표시 길이에서 간격 제거 + + 너무 작은 조각이 사라지는 문제 방지용 + 최소 길이 1 적용 */ - const clickedElement = event.target; + const visibleLength = + Math.max( + segmentLength + - segmentGap, + 1 + ); + + + // SVG circle 생성 + const segment = + document.createElementNS( + "http://www.w3.org/2000/svg", + "circle" + ); + + + // 기본 원 위치 설정 + segment.setAttribute( + "cx", + centerX + ); + + segment.setAttribute( + "cy", + centerY + ); + + segment.setAttribute( + "r", + radius + ); + + + // CSS 클래스 추가 + segment.classList.add( + "emotion-chart-segment" + ); + + + // 색상 그룹 클래스 추가 + segment.classList.add( + `emotion-chart-segment--${group.colorGroup}` + ); + + + // 실제 stroke 색상 적용 + segment.style.stroke = + emotionChartColors[ + group.colorGroup + ] + || emotionChartColors.default; /* - 클릭한 위치가 기간 설정 버튼 내부인지 확인합니다. + 현재 조각 길이 설정 + + 첫 번째 값 + → 실제 표시되는 선 길이 - 버튼의 글자 부분을 클릭해도 true가 됩니다. + 두 번째 값 + → 나머지 비어 있는 선 길이 */ - const clickedToggleButton = - periodToggleButton.contains(clickedElement); + segment.style.strokeDasharray = + `${visibleLength} ${ + circumference + - visibleLength + }`; + + + // 현재 조각 시작 위치 설정 + segment.style.strokeDashoffset = + `${-currentOffset}`; /* - 클릭한 위치가 기간 설정창 내부인지 확인합니다. + hover에서 사용할 감정 이름 저장 + + 예: + "기쁨|만족|신남" + */ + segment.dataset.emotionNames = + group.emotionNames.join("|"); - 다음 요소를 클릭했을 때 true가 됩니다. - - 시작 날짜 입력창 - - 종료 날짜 입력창 - - 적용 버튼 - - 초기화 버튼 - - 기간 설정창의 빈 공간 + /* + 마우스 진입 시 툴팁 표시 */ - const clickedInsidePeriodFilter = - periodFilter.contains(clickedElement); + segment.addEventListener( + "mouseenter", + (event) => { + + showEmotionChartTooltip( + group.emotionNames, + event + ); + } + ); /* - 기간 설정 버튼도 아니고, - 기간 설정창 내부도 아니라면 - 팝업 바깥을 클릭한 것입니다. + 조각 위에서 마우스 이동 시 + 툴팁 위치도 함께 이동 */ - if ( - !clickedToggleButton - && !clickedInsidePeriodFilter - ) { - closePeriodFilter(); - } + segment.addEventListener( + "mousemove", + (event) => { + + showEmotionChartTooltip( + group.emotionNames, + event + ); + } + ); + + + /* + 마우스 이탈 시 툴팁 제거 + */ + segment.addEventListener( + "mouseleave", + hideEmotionChartTooltip + ); + + + // SVG에 조각 추가 + emotionChartSegments.appendChild( + segment + ); + + + // 다음 조각 시작 위치 계산 + currentOffset += + segmentLength; }); } /* ========================================================= - 20. 감정별 기록 팝업 요소 + 25. 감정 도넛 그래프 초기 생성 + ========================================================= */ + + renderEmotionChart(); + + + /* ========================================================= + 26. 감정별 기록 팝업 요소 ========================================================= */ - const emotionButtons = - document.querySelectorAll(".emotion-bubble"); + /* + 기존 emotion_modal.html 기능 유지. + + 현재 도넛 그래프에서는 + 감정 클릭 기능을 연결하지 않음. + + 추후 그래프 클릭 또는 별도 감정 버튼 추가 시 + openEmotionModal() 재사용 가능. + */ const modal = - document.querySelector("#emotion-modal"); + document.querySelector( + "#emotion-modal" + ); const modalName = - document.querySelector("#emotion-modal-name"); + document.querySelector( + "#emotion-modal-name" + ); const modalRecords = - document.querySelectorAll(".emotion-modal-record"); + document.querySelectorAll( + ".emotion-modal-record" + ); const modalEmpty = - document.querySelector("#emotion-modal-empty"); + document.querySelector( + "#emotion-modal-empty" + ); const closeButtons = - document.querySelectorAll("[data-modal-close]"); + document.querySelectorAll( + "[data-modal-close]" + ); /* ========================================================= - 21. 감정별 기록 팝업 열기 + 27. 감정별 기록 팝업 열기 ========================================================= */ - function openEmotionModal(emotionId, emotionName) { - - /* - 감정별 기록 팝업 요소가 없다면 - 함수를 실행하지 않습니다. - */ - if (!modal || !modalName || !modalEmpty) { + function openEmotionModal( + emotionId, + emotionName + ) { + + // 팝업 요소 없는 경우 종료 + if ( + !modal + || !modalName + || !modalEmpty + ) { return; } @@ -790,20 +1182,22 @@ document.addEventListener("DOMContentLoaded", () => { let visibleRecordCount = 0; - /* - 팝업 제목에 선택한 감정 이름을 넣습니다. - */ - modalName.textContent = emotionName; + // 팝업 감정 이름 설정 + modalName.textContent = + emotionName; - /* - 선택한 감정과 일치하는 기록만 표시합니다. - */ + // 선택 감정 기록만 표시 modalRecords.forEach((record) => { + const isMatched = - record.dataset.recordEmotion === emotionId; + record.dataset.recordEmotion + === emotionId; + + + record.hidden = + !isMatched; - record.hidden = !isMatched; if (isMatched) { visibleRecordCount += 1; @@ -811,59 +1205,51 @@ document.addEventListener("DOMContentLoaded", () => { }); - /* - 표시할 기록이 없다면 빈 안내 문구를 표시합니다. - */ - modalEmpty.hidden = visibleRecordCount !== 0; + // 기록 없음 문구 처리 + modalEmpty.hidden = + visibleRecordCount !== 0; - /* - 감정별 기록 팝업을 표시합니다. - */ + // 팝업 표시 modal.hidden = false; - /* - 팝업 뒤쪽 페이지의 스크롤을 막습니다. - */ - document.body.classList.add("modal-open"); + // 배경 스크롤 차단 + document.body.classList.add( + "modal-open" + ); } /* ========================================================= - 22. 감정별 기록 팝업 닫기 + 28. 감정별 기록 팝업 닫기 ========================================================= */ function closeEmotionModal() { + + // 팝업 없는 경우 종료 if (!modal) { return; } - modal.hidden = true; - - document.body.classList.remove("modal-open"); - } + // 팝업 숨김 + modal.hidden = true; - /* ========================================================= - 23. 감정 조약돌 클릭 - ========================================================= */ - emotionButtons.forEach((button) => { - button.addEventListener("click", () => { - openEmotionModal( - button.dataset.emotionId, - button.dataset.emotionName - ); - }); - }); + // 배경 스크롤 복구 + document.body.classList.remove( + "modal-open" + ); + } /* ========================================================= - 24. 감정 팝업 닫기 버튼 및 배경 클릭 + 29. 감정 팝업 닫기 ========================================================= */ closeButtons.forEach((button) => { + button.addEventListener( "click", closeEmotionModal @@ -872,56 +1258,66 @@ document.addEventListener("DOMContentLoaded", () => { /* ========================================================= - 25. Escape 키로 열린 요소 닫기 + 30. Escape 키 처리 ========================================================= */ - document.addEventListener("keydown", (event) => { + document.addEventListener( + "keydown", + (event) => { - /* - 누른 키가 Escape가 아니라면 - 아래 코드를 실행하지 않습니다. - */ - if (event.key !== "Escape") { - return; - } + // Escape 아닌 경우 종료 + if (event.key !== "Escape") { + return; + } - /* - 감정별 기록 팝업이 열려 있다면 닫습니다. - */ - if (modal && !modal.hidden) { - closeEmotionModal(); - } + // 감정 팝업 닫기 + if ( + modal + && !modal.hidden + ) { + closeEmotionModal(); + } - /* - 장소별 기록 팝업이 열려 있다면 닫습니다. - */ - if (locationModal && !locationModal.hidden) { - closeLocationModal(); - } + // 장소 팝업 닫기 + if ( + locationModal + && !locationModal.hidden + ) { + closeLocationModal(); + } - /* - 지도 점 세 개 설정 메뉴가 열려 있다면 닫습니다. - */ - if (mapSettingsMenu && !mapSettingsMenu.hidden) { - mapSettingsMenu.hidden = true; + // 지도 설정 메뉴 닫기 + if ( + mapSettingsMenu + && !mapSettingsMenu.hidden + ) { + mapSettingsMenu.hidden = true; - if (mapSettingsButton) { - mapSettingsButton.setAttribute( - "aria-expanded", - "false" - ); + + if (mapSettingsButton) { + + mapSettingsButton.setAttribute( + "aria-expanded", + "false" + ); + } } - } - /* - 기간 설정창이 열려 있다면 닫습니다. - */ - if (periodFilter && !periodFilter.hidden) { - closePeriodFilter(); + // 기간 설정창 닫기 + if ( + periodFilter + && !periodFilter.hidden + ) { + closePeriodFilter(); + } + + + // 감정 툴팁 제거 + hideEmotionChartTooltip(); } - }); + ); }); \ No newline at end of file diff --git a/diary/templates/diary/partials/emotion_list.html b/diary/templates/diary/partials/emotion_list.html index bd9b4ea..3c956d3 100644 --- a/diary/templates/diary/partials/emotion_list.html +++ b/diary/templates/diary/partials/emotion_list.html @@ -41,11 +41,11 @@

- {{ records|length }}개 + {{ total_record_count }}개
@@ -111,14 +111,14 @@

========================== -->
Date: Fri, 7 Aug 2026 12:07:42 +0900 Subject: [PATCH 06/13] =?UTF-8?q?style:=20=EA=B0=90=EC=A0=95=EB=B3=84=20?= =?UTF-8?q?=EB=8F=84=EB=84=9B=20=EA=B7=B8=EB=9E=98=ED=94=84=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/static/diary/css/diary.css | 744 ++++++++++++------------------- 1 file changed, 294 insertions(+), 450 deletions(-) diff --git a/diary/static/diary/css/diary.css b/diary/static/diary/css/diary.css index ab0ad27..731e74d 100644 --- a/diary/static/diary/css/diary.css +++ b/diary/static/diary/css/diary.css @@ -3,22 +3,13 @@ ========================================================= */ .diary-list-container { - /* - 페이지 콘텐츠의 최대 너비입니다. - - 화면이 1280px 이상이면 최대 1200px, - 그보다 좁으면 좌우에 각각 40px 여백을 남깁니다. - */ + /* 페이지 콘텐츠 최대 너비 */ width: min(1200px, calc(100% - 80px)); - /* - 컨테이너를 화면 가로 중앙에 배치합니다. - */ + /* 화면 가로 중앙 배치 */ margin: 0 auto; - /* - 위쪽 28px, 아래쪽 50px의 내부 여백입니다. - */ + /* 위아래 내부 여백 */ padding: 28px 0 50px; } @@ -28,26 +19,19 @@ ========================================================= */ .diary-tabs-wrapper { - /* - 탭 묶음을 가로 중앙에 배치합니다. - */ + /* 탭 중앙 배치 */ display: flex; justify-content: center; width: 100%; - /* - 탭과 아래 콘텐츠 사이의 간격입니다. - */ + /* 탭과 콘텐츠 사이 간격 */ margin-bottom: 12px; } .diary-tabs { - /* - 각 탭 버튼을 한 줄로 배치합니다. - 내용만큼만 너비를 차지합니다. - */ + /* 탭 한 줄 배치 */ display: inline-flex; align-items: center; gap: 4px; @@ -64,9 +48,7 @@ .diary-tab { - /* - a 태그 또는 버튼으로 만들어진 각각의 탭입니다. - */ + /* 각 탭 기본 스타일 */ padding: 10px 18px; border: 0; @@ -83,9 +65,7 @@ .diary-tab.active { - /* - 현재 선택된 탭입니다. - */ + /* 현재 선택된 탭 */ background: #fff4cb; color: #222222; @@ -98,50 +78,31 @@ ========================================================= */ .diary-list-header { - /* - 전체 기록 제목, 기록 개수, 필터를 - 왼쪽부터 가로 한 줄로 배치합니다. - - 화면: - 전체 기록 7개 최신순 기간 설정 - */ + /* 제목과 필터 가로 배치 */ display: flex; align-items: center; justify-content: flex-start; - /* - 제목·개수 묶음과 필터 사이의 간격입니다. - - 값을 키우면 필터가 제목에서 더 멀어집니다. - */ + /* 제목과 필터 사이 간격 */ gap: 18px; - /* - 헤더와 카드 목록 사이의 간격입니다. - */ + /* 카드 목록과의 간격 */ margin-bottom: 24px; } .diary-list-heading { - /* - '전체 기록'과 '7개'를 같은 줄에 배치합니다. - */ + /* 제목과 기록 개수 한 줄 배치 */ display: flex; align-items: center; gap: 10px; - /* - 남는 공간을 불필요하게 차지하지 않습니다. - */ flex: 0 0 auto; } .diary-list-title { - /* - h1 태그의 브라우저 기본 여백을 제거합니다. - */ + /* h1 기본 여백 제거 */ margin: 0; color: #222222; @@ -152,10 +113,19 @@ } +.diary-list-description { + /* 제목 아래 설명 문구 */ + margin: 8px 0 0; + + color: #8b857d; + + font-size: 14px; + line-height: 1.5; +} + + .diary-list-count { - /* - 기록 개수를 작은 캡슐 모양으로 표시합니다. - */ + /* 기록 개수 캡슐 */ padding: 5px 10px; border-radius: 999px; @@ -167,9 +137,6 @@ font-weight: 700; line-height: 1; - /* - '7'과 '개'가 서로 다른 줄로 나뉘지 않게 합니다. - */ white-space: nowrap; } @@ -180,31 +147,21 @@ .diary-filter-form, .diary-list-controls { - /* - 최신순 선택창과 기간 설정 버튼을 - 같은 줄에 배치합니다. - */ + /* 필터 버튼 가로 배치 */ display: flex; align-items: center; gap: 8px; - /* - 기간 설정 팝업의 위치 기준이 됩니다. - */ + /* 기간 설정창 위치 기준 */ position: relative; - /* - 필터 영역이 남은 가로 공간 전체를 차지하지 않습니다. - */ flex: 0 0 auto; } .diary-sort-select, .period-toggle-button { - /* - 최신순 선택창과 기간 설정 버튼의 공통 스타일입니다. - */ + /* 필터 공통 스타일 */ min-height: 42px; padding: 0 16px; @@ -214,9 +171,6 @@ background: #ffffff; color: #333333; - /* - 페이지에서 사용하는 글꼴을 그대로 상속합니다. - */ font: inherit; cursor: pointer; @@ -226,10 +180,6 @@ .diary-sort-select:focus, .period-toggle-button:focus, .period-filter input:focus { - /* - 키보드나 마우스로 요소를 선택했을 때 - 노란색 외곽선을 표시합니다. - */ /* outline: 2px solid #e9c767; outline-offset: 2px; @@ -238,9 +188,7 @@ .period-toggle-button.active { - /* - 기간 설정 창이 열려 있을 때의 버튼입니다. - */ + /* 기간 설정 활성 상태 */ border-color: #e1bd55; background: #fff7d9; @@ -250,57 +198,22 @@ /* ========================================================= - 기간 설정 팝업 + 5. 기간 설정 팝업 ========================================================= */ .period-filter { - /* - 최신순 선택창과 기간 설정 버튼을 감싸는 - .diary-filter-form을 기준으로 배치됩니다. - - 기존에는 필터 아래쪽에 표시되었지만, - 이제는 필터의 오른쪽에 나란히 표시합니다. - */ + /* 필터 폼 기준 절대 배치 */ position: absolute; z-index: 50; - /* - top: 0 - → 최신순·기간 설정 버튼과 날짜 선택창의 - 위쪽 높이를 똑같이 맞춥니다. - - 값을 키우면 날짜 선택창이 아래로 내려가고, - 음수로 바꾸면 위로 올라갑니다. - */ top: 0; - - /* - left: calc(100% + 16px) - → 필터 폼의 오른쪽 끝에서 16px 떨어진 위치에 - 날짜 선택창을 표시합니다. - - 16px을 크게 하면 기간 설정 버튼과 더 멀어지고, - 작게 하면 버튼과 더 가까워집니다. - */ left: calc(100% + 16px); - - /* - 기존의 right: 0을 해제합니다. - - right: 0이 남아 있으면 - left와 right가 동시에 적용되어 - 너비나 위치가 예상과 다르게 계산될 수 있습니다. - */ right: auto; display: flex; align-items: flex-end; gap: 10px; - /* - 시작일·종료일·버튼이 들어갈 최소 너비입니다. - 현재 화면에서는 약 500px 정도가 적당합니다. - */ min-width: 500px; padding: 18px; @@ -315,18 +228,13 @@ .period-filter[hidden] { - /* - hidden 속성이 있을 때 완전히 숨깁니다. - */ + /* hidden 상태 제거 */ display: none; } .period-input-group { - /* - 시작일 또는 종료일의 label과 input을 - 위아래로 배치합니다. - */ + /* 라벨과 날짜 입력창 세로 배치 */ display: flex; flex-direction: column; gap: 7px; @@ -354,9 +262,7 @@ .period-separator { - /* - 시작일과 종료일 사이의 물결표입니다. - */ + /* 시작일과 종료일 구분 */ padding-bottom: 10px; color: #999999; @@ -365,9 +271,7 @@ .period-apply-button, .period-reset-button { - /* - 적용 버튼과 초기화 버튼의 공통 스타일입니다. - */ + /* 적용 / 초기화 공통 스타일 */ display: inline-flex; align-items: center; justify-content: center; @@ -405,9 +309,7 @@ ========================================================= */ .diary-grid { - /* - 기본 화면에서 한 줄에 카드 5개를 배치합니다. - */ + /* 한 줄에 카드 5개 배치 */ display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 22px; @@ -417,9 +319,6 @@ .diary-card { - /* - 카드가 grid 칸보다 커지는 것을 방지합니다. - */ min-width: 0; overflow: hidden; @@ -440,9 +339,7 @@ .diary-card:hover { - /* - 마우스를 올리면 카드가 살짝 위로 이동합니다. - */ + /* hover 시 카드 이동 */ transform: translateY(-5px); box-shadow: @@ -452,9 +349,7 @@ .diary-card-link { - /* - 카드 전체 영역을 클릭할 수 있게 합니다. - */ + /* 카드 전체 클릭 영역 */ display: block; height: 100%; @@ -469,16 +364,11 @@ ========================================================= */ .diary-card-image-wrapper { - /* - 카드 이미지 영역입니다. - */ position: relative; width: 100%; - /* - 이미지 영역의 가로:세로 비율을 4:3으로 유지합니다. - */ + /* 이미지 4:3 비율 유지 */ aspect-ratio: 4 / 3; overflow: hidden; @@ -493,10 +383,7 @@ width: 100%; height: 100%; - /* - 사진이 이미지 영역을 빈틈없이 채우도록 합니다. - 넘치는 부분은 잘립니다. - */ + /* 이미지 영역 채우기 */ object-fit: cover; transition: transform 0.3s ease; @@ -504,17 +391,13 @@ .diary-card:hover .diary-card-image { - /* - 카드에 마우스를 올리면 사진이 살짝 확대됩니다. - */ + /* hover 시 이미지 확대 */ transform: scale(1.04); } .diary-card-image-placeholder { - /* - 사진이 없을 때 보이는 대체 영역입니다. - */ + /* 이미지 없음 영역 */ display: flex; align-items: center; justify-content: center; @@ -534,9 +417,7 @@ ========================================================= */ .diary-card-content { - /* - 날짜, 기록 내용, 장소 줄을 위아래로 배치합니다. - */ + /* 날짜 / 내용 / 장소 세로 배치 */ display: flex; flex-direction: column; @@ -556,9 +437,7 @@ .diary-card-title { - /* - 기록 내용을 최대 두 줄까지만 보여줍니다. - */ + /* 기록 내용 최대 두 줄 제한 */ display: -webkit-box; min-height: 44px; @@ -584,35 +463,20 @@ ========================================================= */ .diary-card-bottom-row { - /* - 장소는 왼쪽, - 감정 아이콘은 오른쪽에 배치합니다. - */ + /* 장소 왼쪽 / 감정 오른쪽 */ display: flex; align-items: center; justify-content: space-between; - /* - 장소 이름과 감정 아이콘 사이의 최소 간격입니다. - */ gap: 12px; - /* - 제목과 장소 줄 사이의 간격입니다. - */ margin-top: 12px; } .diary-card-location { - /* - 장소 영역이 남은 공간을 사용합니다. - */ flex: 1; - /* - flex 자식의 글자가 정상적으로 줄어들 수 있게 합니다. - */ min-width: 0; margin: 0; @@ -630,23 +494,14 @@ .diary-card-location--empty { - /* - 장소 정보가 없을 때 문구를 더 연하게 표시합니다. - */ + /* 장소 없음 스타일 */ color: #aaa397; } .diary-card-emotion { - /* - 기존의 absolute 배치를 해제하고, - 장소 오른쪽에서 일반적인 요소로 배치합니다. - */ position: static; - /* - 장소 이름이 길어져도 아이콘이 작아지지 않게 합니다. - */ flex-shrink: 0; display: flex; @@ -670,9 +525,7 @@ ========================================================= */ .diary-empty { - /* - 빈 화면 안내가 grid 전체 열을 차지하게 합니다. - */ + /* grid 전체 사용 */ grid-column: 1 / -1; padding: 80px 20px; @@ -736,286 +589,267 @@ ========================================================= */ .emotion-page-header { - /* - 왼쪽의 제목과 오른쪽의 이번 달 감정 요약을 - 양쪽에 나누어 배치합니다. - */ + /* 감정별 제목 영역 */ display: flex; align-items: flex-start; justify-content: space-between; - gap: 30px; - margin-bottom: 30px; + margin-bottom: 12px; } -.emotion-month-summary { +.emotion-page-heading { + /* 제목과 설명 세로 배치 */ display: flex; - align-items: center; - gap: 10px; - - padding: 12px 18px; - - border: 1px solid #eadfce; - border-radius: 14px; + flex-direction: column; +} - background: #ffffff; - font-size: 13px; -} +/* ========================================================= + 12. 감정 도넛 그래프 전체 영역 + ========================================================= */ +.emotion-chart-section { + /* 그래프 중앙 배치 */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; -.emotion-month-summary strong { - color: #d98d36; + width: 100%; - font-size: 18px; + min-height: 500px; } -.top-emotion-list { +/* ========================================================= + 13. 감정 도넛 그래프 wrapper + ========================================================= */ + +.emotion-chart-wrapper { /* - 공동 1위 감정이 여러 개일 경우 - 한 줄 또는 여러 줄로 자연스럽게 표시 + SVG / 중앙 기록 수 / 툴팁 + 위치 기준 */ - display: flex; - align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - gap: 4px; + position: relative; + + width: min(460px, 100%); + + aspect-ratio: 1 / 1; } -.top-emotion-name { - color: #d98d36; +/* ========================================================= + 14. SVG 감정 도넛 그래프 + ========================================================= */ - white-space: nowrap; -} +.emotion-chart { + display: block; + width: 100%; + height: 100%; -.top-emotion-separator { - color: #b7aa97; + overflow: visible; } -.emotion-bubble-area { +/* ========================================================= + 15. 도넛 그래프 배경 원 + ========================================================= */ + +.emotion-chart-background { /* - 감정 조약돌들의 absolute 배치 기준입니다. + 기록이 없거나 + 색상 조각 뒤쪽에 보이는 기본 원 */ - position: relative; - - width: 100%; - min-height: 560px; + fill: none; - border-radius: 24px; + stroke: #ebe8e2; + stroke-width: 46; - /*background: #ffffff;*/ + pointer-events: none; } -.emotion-bubble { - position: absolute; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 4px; +/* ========================================================= + 16. 도넛 그래프 색상 조각 + ========================================================= */ +.emotion-chart-segment { /* - 기존 테두리를 제거 + 원 내부 채움 제거 + stroke만 사용해 도넛 생성 */ - border: none; + fill: none; /* - 조약돌 모양은 그대로 유지 + 배경 원과 동일한 두께 적용 */ - border-radius: 42% 58% 53% 47% / 50% 50% 50% 50%; - - background: rgba(255, 250, 231, 0.95); - color: #333333; + stroke-width: 46; /* - 테두리 대신 기본 상태에서도 그림자를 표시합니다. + 각 조각 끝을 둥글게 처리 + */ + stroke-linecap: round; - 0 → 좌우 이동 없음 - 5px → 아래로 5px 이동 - 14px → 그림자 흐림 정도 - rgba → 검정색, 투명도 12% + /* + 원 둘레 stroke 영역에서만 + 마우스 이벤트 발생 */ - box-shadow: 0 5px 14px rgba(0, 0, 0, 0.12); + pointer-events: stroke; - font: inherit; cursor: pointer; transition: - transform 0.2s ease, - box-shadow 0.2s ease; + stroke-width 0.2s ease, + opacity 0.2s ease, + filter 0.2s ease; } -.emotion-bubble:hover { - z-index: 5; - - transform: scale(1.08); - - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12); -} - - -.emotion-bubble-name { - font-size: 20px; - font-weight: 700; -} - - -.emotion-bubble-count { - color: #8e877c; - - font-size: 15px; -} - - -/* 감정별 색상 */ - -.emotion-bubble--yellow { - /*border-color: #f9d472;*/ - - background: #fff7e3; -} - - -.emotion-bubble--blue { - /*border-color: #dae6f3;*/ +.emotion-chart-segment:hover { + /* + hover 조각 강조 + */ + stroke-width: 51; - background: #eff7ff; + filter: brightness(0.97); } -.emotion-bubble--pink { - /*border-color: #f8b3b4;*/ - - background: #fff2f2; -} - +/* ========================================================= + 17. 감정 그래프 중앙 기록 수 + ========================================================= */ -.emotion-bubble--purple { - /*border-color: #9d72ce;*/ +.emotion-chart-center { + /* + SVG 중앙 위에 텍스트 배치 + */ + position: absolute; - background: #f8f1ff; -} + top: 50%; + left: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 7px; -.emotion-bubble--red { - /*border-color: #ce7772;*/ + transform: translate(-50%, -50%); - background: #fff4f1; + /* + SVG hover 이벤트 방해 제거 + */ + pointer-events: none; } -.emotion-bubble--orange { - /*border-color: #cea972;*/ +.emotion-chart-center-label { + color: #817b72; - background: #fff9f1; + font-size: 14px; + font-weight: 500; + line-height: 1; } -.emotion-bubble--default { - /*border-color: #c5c5c5;*/ +.emotion-chart-center-count { + color: #24211d; - background: #dedede; + font-size: 36px; + font-weight: 700; + line-height: 1; } -/* 기록 개수에 따른 감정 조약돌 크기 */ - -.emotion-bubble--large { - width: 130px; - height: 130px; -} - +/* ========================================================= + 18. 감정 그래프 툴팁 + ========================================================= */ -.emotion-bubble--medium { - width: 92px; - height: 92px; -} +.emotion-chart-tooltip { + /* + 그래프 wrapper 기준 배치 + */ + position: absolute; + z-index: 20; + /* + JS에서 마우스 위치에 맞춰 + left / top 값 변경 + */ + left: 0; + top: 0; -.emotion-bubble--small { - width: 64px; - height: 64px; -} + /* + 툴팁이 마우스 정중앙에 위치하도록 조정 + */ + transform: translate(-50%, -100%); + padding: 9px 13px; -/* 감정 조약돌의 임시 배치 위치 */ + border: 1px solid #e2ddd4; + border-radius: 10px; -.emotion-bubble:nth-child(1) { - top: 30px; - left: 9%; -} + background: rgba(255, 255, 255, 0.96); + color: #37332e; + font-size: 13px; + font-weight: 600; + line-height: 1.3; -.emotion-bubble:nth-child(2) { - top: 70px; - left: 25%; -} + white-space: nowrap; + pointer-events: none; -.emotion-bubble:nth-child(3) { - top: 160px; - left: 15%; + box-shadow: + 0 7px 20px rgba(0, 0, 0, 0.1); } -.emotion-bubble:nth-child(4) { - top: 110px; - left: 44%; +.emotion-chart-tooltip[hidden] { + /* 툴팁 숨김 */ + display: none; } -.emotion-bubble:nth-child(5) { - top: 260px; - left: 34%; -} +/* ========================================================= + 19. 감정 기록 없음 + ========================================================= */ +.emotion-chart-empty { + /* 그래프 아래 빈 기록 안내 */ + margin-top: 12px; -.emotion-bubble:nth-child(6) { - top: 80px; - left: 68%; + text-align: center; } -.emotion-bubble:nth-child(7) { - top: 210px; - left: 75%; -} +.emotion-chart-empty-title { + margin: 0 0 7px; + color: #403c36; -.emotion-bubble:nth-child(8) { - top: 340px; - left: 60%; + font-size: 16px; + font-weight: 700; } -.emotion-bubble:nth-child(9) { - top: 390px; - left: 20%; -} +.emotion-chart-empty-description { + margin: 0; + color: #918a81; -.emotion-bubble:nth-child(10) { - top: 370px; - left: 80%; + font-size: 13px; } /* ========================================================= - 12. 감정별 기록 팝업 + 20. 감정별 기록 팝업 ========================================================= */ body.modal-open { - /* - 팝업이 열렸을 때 뒤쪽 페이지가 스크롤되지 않게 합니다. - */ + /* 팝업 뒤 페이지 스크롤 차단 */ overflow: hidden; } @@ -1120,13 +954,11 @@ body.modal-open { /* ========================================================= - 13. 장소별 기록 화면 및 지도 설정 + 21. 장소별 기록 화면 및 지도 설정 ========================================================= */ .location-map-section { - /* - 지도 설정 툴바와 지도를 위아래로 배치합니다. - */ + /* 툴바와 지도 세로 배치 */ display: flex; flex-direction: column; gap: 10px; @@ -1136,9 +968,7 @@ body.modal-open { .map-toolbar { - /* - 점 세 개 버튼을 오른쪽 끝에 배치합니다. - */ + /* 설정 버튼 오른쪽 배치 */ display: flex; align-items: center; justify-content: flex-end; @@ -1148,9 +978,7 @@ body.modal-open { .map-display-settings { - /* - 설정 메뉴가 점 세 개 버튼을 기준으로 표시되게 합니다. - */ + /* 설정 메뉴 위치 기준 */ position: relative; z-index: 30; } @@ -1188,8 +1016,6 @@ body.modal-open { .map-settings-button:hover { transform: translateY(-1px); - /*background: #fff8df;*/ - box-shadow: 0 5px 14px rgba(0, 0, 0, 0.12); } @@ -1201,9 +1027,7 @@ body.modal-open { .map-settings-menu { - /* - 점 세 개 버튼 바로 아래에 표시되는 설정 메뉴입니다. - */ + /* 점 세 개 버튼 아래 배치 */ position: absolute; z-index: 40; @@ -1269,13 +1093,11 @@ body.modal-open { /* ========================================================= - 14. 서울 지도 + 22. 서울 지도 ========================================================= */ .location-map-wrapper { - /* - 서울 지도 SVG의 크기와 가운데 정렬을 담당합니다. - */ + /* 지도 크기 및 중앙 정렬 */ position: relative; width: min(800px, 100%); @@ -1293,9 +1115,7 @@ body.modal-open { .seoul-map path { - /* - 모든 서울 구역 path의 기본 스타일입니다. - */ + /* 서울 구역 기본 스타일 */ fill: #ffffff; stroke: #777777; stroke-width: 1; @@ -1309,18 +1129,13 @@ body.modal-open { .seoul-map path:focus { - /* - SVG 구역을 클릭했을 때 - 기본 브라우저 외곽선을 제거합니다. - */ + /* SVG 기본 포커스 제거 */ outline: none; } .seoul-map path.has-record { - /* - 기록이 있는 구역에 적용됩니다. - */ + /* 기록 있는 구역 */ stroke-width: 2; cursor: pointer; @@ -1328,20 +1143,13 @@ body.modal-open { .seoul-map path.has-record:hover { - /* - 기록이 있는 구역에 마우스를 올렸을 때 - 살짝 어두워지고 테두리가 굵어집니다. - */ - /*stroke-width: 3;*/ - + /* 기록 있는 구역 hover */ filter: brightness(0.94); } .district-labels { - /* - 구 이름 텍스트가 지도 클릭을 방해하지 않게 합니다. - */ + /* 지도 라벨 클릭 방해 제거 */ pointer-events: none; } @@ -1358,16 +1166,12 @@ body.modal-open { text-anchor: middle; dominant-baseline: middle; - /* - 흰색 외곽선을 먼저 그리고 - 그 위에 글자를 표시합니다. - */ paint-order: stroke; } /* ========================================================= - 15. 장소별 기록 팝업 + 23. 장소별 기록 팝업 ========================================================= */ .location-modal { @@ -1470,115 +1274,155 @@ body.modal-open { /* ========================================================= - 16. 반응형 스타일 + 24. 반응형 스타일 ========================================================= */ -/* - 화면 너비가 1200px 이하일 때 - 카드 개수를 한 줄에 4개로 줄입니다. -*/ +/* 1200px 이하 */ @media (max-width: 1200px) { + + /* 카드 한 줄 4개 */ .diary-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } } -/* - 화면 너비가 900px 이하일 때 - 카드와 팝업 목록을 한 줄에 3개로 줄입니다. -*/ +/* 900px 이하 */ @media (max-width: 900px) { + + /* 카드 및 팝업 한 줄 3개 */ .diary-grid, .emotion-modal-grid, .location-modal-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } + + + /* 감정 그래프 크기 축소 */ + .emotion-chart-wrapper { + width: min(420px, 100%); + } } -/* - 화면 너비가 760px 이하일 때 - 전체 기록 제목과 필터를 위아래로 배치합니다. -*/ +/* 760px 이하 */ @media (max-width: 760px) { + .diary-list-container { - /* - 작은 화면에서는 좌우 여백을 각각 20px로 줄입니다. - */ + /* 좌우 여백 축소 */ width: calc(100% - 40px); } + .diary-list-header { + /* 제목과 필터 세로 배치 */ align-items: flex-start; flex-direction: column; gap: 14px; } + .diary-filter-form, .diary-list-controls { width: 100%; } + .period-filter { - /* - 작은 화면에서는 오른쪽에 표시할 공간이 부족하므로 - 다시 필터 버튼 아래쪽에 표시합니다. - */ + /* 기간 설정창 아래 배치 */ top: calc(100% + 10px); - /* - 필터 폼의 왼쪽에 날짜창을 맞춥니다. - */ left: 0; right: auto; - /* - 날짜 입력창과 버튼이 공간이 부족할 때 - 다음 줄로 내려가도록 합니다. - */ flex-wrap: wrap; - /* - 모바일 화면보다 날짜창이 커지지 않게 제한합니다. - */ - min-width: min(500px, calc(100vw - 40px)); + min-width: min( + 500px, + calc(100vw - 40px) + ); + } + + + .emotion-page-header { + /* 감정 제목 세로 배치 */ + flex-direction: column; + } + + + .emotion-chart-section { + /* 모바일 그래프 높이 축소 */ + min-height: 450px; + } + + + .emotion-chart-wrapper { + width: min(400px, 100%); } } -/* - 화면 너비가 650px 이하일 때 - 카드 및 팝업 목록을 한 줄에 2개로 줄입니다. -*/ +/* 650px 이하 */ @media (max-width: 650px) { + .diary-grid, .emotion-modal-grid, .location-modal-grid { + /* 카드 및 팝업 한 줄 2개 */ grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; } + .diary-card-content { min-height: 118px; padding: 12px; } + .diary-card-title { font-size: 14px; } - .emotion-page-header { - flex-direction: column; + + .emotion-chart-section { + min-height: 390px; + } + + + .emotion-chart-wrapper { + width: min(350px, 100%); + } + + + .emotion-chart-background, + .emotion-chart-segment { + /* 작은 화면에서 도넛 두께 축소 */ + stroke-width: 40; + } + + + .emotion-chart-segment:hover { + stroke-width: 44; + } + + + .emotion-chart-center-count { + /* 가운데 기록 수 크기 축소 */ + font-size: 30px; } - .emotion-bubble-area { - min-height: 720px; + + .emotion-chart-center-label { + font-size: 13px; } + .emotion-modal-content, .location-modal-content { width: calc(100% - 28px); + padding: 20px; } } \ No newline at end of file From 8869098a211083d509a549577b0a05f24ee11728 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 12:36:53 +0900 Subject: [PATCH 07/13] =?UTF-8?q?style:=20=EA=B0=90=EC=A0=95=20=EA=B7=B8?= =?UTF-8?q?=EB=9E=98=ED=94=84=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EA=B0=90=EC=A0=95=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/static/diary/css/diary.css | 373 +++++++++++-- diary/static/diary/js/diary.js | 506 ++++++++---------- .../diary/partials/emotion_list.html | 275 +++++----- 3 files changed, 683 insertions(+), 471 deletions(-) diff --git a/diary/static/diary/css/diary.css b/diary/static/diary/css/diary.css index 731e74d..49bec31 100644 --- a/diary/static/diary/css/diary.css +++ b/diary/static/diary/css/diary.css @@ -610,15 +610,13 @@ ========================================================= */ .emotion-chart-section { - /* 그래프 중앙 배치 */ display: flex; - flex-direction: column; align-items: center; justify-content: center; width: 100%; - min-height: 500px; + min-height: 360px; } @@ -627,18 +625,17 @@ ========================================================= */ .emotion-chart-wrapper { - /* - SVG / 중앙 기록 수 / 툴팁 - 위치 기준 - */ position: relative; - width: min(460px, 100%); + /* 참고 이미지에 가까운 크기 */ + width: 360px; + max-width: 100%; aspect-ratio: 1 / 1; } + /* ========================================================= 14. SVG 감정 도넛 그래프 ========================================================= */ @@ -654,66 +651,126 @@ /* ========================================================= - 15. 도넛 그래프 배경 원 + 15. 도넛 그래프 배경 ========================================================= */ .emotion-chart-background { - /* - 기록이 없거나 - 색상 조각 뒤쪽에 보이는 기본 원 - */ + /* 도넛 뒤 기본 원 */ fill: none; - stroke: #ebe8e2; - stroke-width: 46; + stroke: #f2eee7; + stroke-width: 52; pointer-events: none; } +/* 그래프 안쪽 원 */ + +.emotion-chart-inner-circle { + fill: #fffdfa; + + stroke: #eee5da; + stroke-width: 2; + + pointer-events: none; + + filter: + drop-shadow( + 0 2px 4px rgba(80, 65, 50, 0.08) + ); +} + + /* ========================================================= - 16. 도넛 그래프 색상 조각 + 16. 도넛 그래프 조각 ========================================================= */ .emotion-chart-segment { - /* - 원 내부 채움 제거 - stroke만 사용해 도넛 생성 - */ fill: none; - /* - 배경 원과 동일한 두께 적용 - */ - stroke-width: 46; + /* 참고 이미지와 비슷한 두꺼운 띠 */ + stroke-width: 52; /* - 각 조각 끝을 둥글게 처리 + 끝을 완전한 반원으로 만들지 않음 + + butt + → 참고 이미지처럼 조각 경계가 깔끔함 */ - stroke-linecap: round; + stroke-linecap: butt; /* - 원 둘레 stroke 영역에서만 - 마우스 이벤트 발생 + 그래프 시작 위치를 + 12시 방향으로 이동 */ + transform: rotate(-90deg); + + transform-origin: 160px 160px; + pointer-events: stroke; cursor: pointer; + filter: + drop-shadow( + 0 2px 3px rgba(74, 58, 45, 0.06) + ); + transition: - stroke-width 0.2s ease, - opacity 0.2s ease, - filter 0.2s ease; + stroke-width 0.18s ease, + filter 0.18s ease, + opacity 0.18s ease; } +/* 감정 색상 */ + +.emotion-chart-segment--yellow { + stroke: #f9dda0; +} + + +.emotion-chart-segment--blue { + stroke: #bcdcf7; +} + + +.emotion-chart-segment--pink { + stroke: #f6c9cf; +} + + +.emotion-chart-segment--purple { + stroke: #cfc2f2; +} + + +.emotion-chart-segment--red { + stroke: #f2b6b2; +} + + +.emotion-chart-segment--orange { + stroke: #f7c995; +} + + +.emotion-chart-segment--default { + stroke: #dedede; +} + + +/* hover 강조 */ + .emotion-chart-segment:hover { - /* - hover 조각 강조 - */ - stroke-width: 51; + stroke-width: 57; - filter: brightness(0.97); + filter: + brightness(1.02) + drop-shadow( + 0 4px 5px rgba(65, 50, 40, 0.11) + ); } @@ -722,9 +779,6 @@ ========================================================= */ .emotion-chart-center { - /* - SVG 중앙 위에 텍스트 배치 - */ position: absolute; top: 50%; @@ -734,31 +788,29 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 7px; + + gap: 9px; transform: translate(-50%, -50%); - /* - SVG hover 이벤트 방해 제거 - */ pointer-events: none; } .emotion-chart-center-label { - color: #817b72; + color: #776f69; - font-size: 14px; - font-weight: 500; - line-height: 1; + font-size: 15px; + font-weight: 600; } .emotion-chart-center-count { - color: #24211d; + color: #8b71d1; + + font-size: 34px; + font-weight: 800; - font-size: 36px; - font-weight: 700; line-height: 1; } @@ -843,6 +895,227 @@ font-size: 13px; } +.emotion-summary-board { + width: 100%; + + padding: + 30px + 28px + 26px; + + border: 1px solid #eee7dd; + border-radius: 28px; + + background: + rgba(255, 253, 248, 0.82); + + box-shadow: + 0 8px 24px rgba(69, 52, 38, 0.06); +} + +.emotion-card-guide { + display: flex; + align-items: center; + justify-content: center; + + gap: 8px; + + margin: + -2px + 0 + 20px; + + color: #9a9188; + + font-size: 12px; +} + + +.emotion-card-guide-line { + width: 45px; + + border-top: + 2px dotted #ddd1ee; +} + + +.emotion-card-guide-heart { + color: #bda8e1; + + font-size: 10px; +} + +/* ========================================================= + 감정별 기록 카드 목록 + ========================================================= */ + +.emotion-record-card-list { + display: flex; + align-items: stretch; + justify-content: center; + flex-wrap: wrap; + + gap: 14px; + + width: 100%; +} + + +/* 감정 카드 */ + +.emotion-record-card { + display: flex; + align-items: center; + + gap: 10px; + + min-width: 150px; + + padding: + 10px + 14px; + + border: 1px solid #eee7dd; + border-radius: 17px; + + background: #ffffff; + + font: inherit; + + cursor: pointer; + + box-shadow: + 0 4px 10px rgba(70, 55, 40, 0.06); + + transition: + transform 0.18s ease, + box-shadow 0.18s ease; +} + + +.emotion-record-card:hover { + transform: translateY(-3px); + + box-shadow: + 0 8px 16px rgba(70, 55, 40, 0.1); +} + + +/* 감정 이미지 */ + +.emotion-record-card-image { + display: flex; + align-items: center; + justify-content: center; + + width: 48px; + height: 48px; + + flex-shrink: 0; +} + + +.emotion-record-card-image img { + display: block; + + width: 44px; + height: 44px; + + object-fit: contain; +} + + +/* 감정 정보 */ + +.emotion-record-card-info { + display: flex; + flex-direction: column; + align-items: flex-start; + + gap: 4px; +} + + +.emotion-record-card-name { + color: #403b36; + + font-size: 14px; + font-weight: 700; +} + + +.emotion-record-card-count { + font-size: 13px; + font-weight: 700; +} + + +/* 카드별 포인트 색상 */ + +.emotion-record-card--blue { + background: #f5faff; +} + + +.emotion-record-card--blue +.emotion-record-card-count { + color: #67a7df; +} + + +.emotion-record-card--pink { + background: #fff7f8; +} + + +.emotion-record-card--pink +.emotion-record-card-count { + color: #e58ea0; +} + + +.emotion-record-card--yellow { + background: #fffdf5; +} + + +.emotion-record-card--yellow +.emotion-record-card-count { + color: #d6aa36; +} + + +.emotion-record-card--purple { + background: #faf7ff; +} + + +.emotion-record-card--purple +.emotion-record-card-count { + color: #9276cf; +} + + +.emotion-record-card--red { + background: #fff7f5; +} + + +.emotion-record-card--red +.emotion-record-card-count { + color: #db827b; +} + + +.emotion-record-card--orange { + background: #fff9f3; +} + + +.emotion-record-card--orange +.emotion-record-card-count { + color: #d99c59; +} /* ========================================================= 20. 감정별 기록 팝업 diff --git a/diary/static/diary/js/diary.js b/diary/static/diary/js/diary.js index 884db7d..442fd57 100644 --- a/diary/static/diary/js/diary.js +++ b/diary/static/diary/js/diary.js @@ -692,9 +692,133 @@ document.addEventListener("DOMContentLoaded", () => { ); } + /* ========================================================= + 20. 감정별 기록 팝업 요소 + ========================================================= */ + + const modal = + document.querySelector("#emotion-modal"); + + const modalName = + document.querySelector("#emotion-modal-name"); + + const modalRecords = + document.querySelectorAll(".emotion-modal-record"); + + const modalEmpty = + document.querySelector("#emotion-modal-empty"); + + const closeButtons = + document.querySelectorAll("[data-modal-close]"); + + + /* ========================================================= + 21. 감정별 기록 팝업 열기 + ========================================================= */ + + function openEmotionModal( + emotionIds, + emotionNames + ) { + + // 필수 요소 없는 경우 종료 + if ( + !modal + || !modalName + || !modalEmpty + ) { + return; + } + + + // 단일 ID도 배열로 변환 + const normalizedIds = + Array.isArray(emotionIds) + ? emotionIds.map(String) + : [String(emotionIds)]; + + + // 감정명도 배열로 변환 + const normalizedNames = + Array.isArray(emotionNames) + ? emotionNames + : [emotionNames]; + + + let visibleRecordCount = 0; + + + // 팝업 제목 설정 + modalName.textContent = + normalizedNames.join(" · "); + + + // 선택 감정 그룹 기록 표시 + modalRecords.forEach((record) => { + + const recordEmotion = + String( + record.dataset.recordEmotion + ); + + + const isMatched = + normalizedIds.includes( + recordEmotion + ); + + + record.hidden = + !isMatched; + + + if (isMatched) { + visibleRecordCount += 1; + } + }); + + + // 기록 없음 문구 처리 + modalEmpty.hidden = + visibleRecordCount !== 0; + + + // 팝업 표시 + modal.hidden = false; + + + // 배경 스크롤 차단 + document.body.classList.add( + "modal-open" + ); + } + + + /* ========================================================= + 22. 감정별 기록 팝업 닫기 + ========================================================= */ + + function closeEmotionModal() { + + // 팝업 없는 경우 종료 + if (!modal) { + return; + } + + + // 팝업 숨김 + modal.hidden = true; + + + // 배경 스크롤 복구 + document.body.classList.remove( + "modal-open" + ); + } + /* ========================================================= - 20. 감정 도넛 그래프 요소 + 23. 감정 도넛 그래프 요소 ========================================================= */ const emotionChart = @@ -724,50 +848,49 @@ document.addEventListener("DOMContentLoaded", () => { /* ========================================================= - 21. 감정 데이터를 색상 그룹별로 합치기 + 24. 색상 그룹별 감정 데이터 생성 ========================================================= */ function getEmotionChartGroups() { - // 색상 그룹별 임시 저장 객체 const groupedData = {}; emotionChartDataItems.forEach( (element) => { - // 감정 이름 + const emotionId = + element.dataset.emotionId.trim(); + const emotionName = element.dataset.emotionName.trim(); - - // 대표 감정 기록 횟수 const emotionCount = Number( element.dataset.emotionCount ); - - // 색상 그룹 이름 const colorGroup = element.dataset.emotionColorGroup.trim(); // 잘못된 데이터 제외 if ( - !emotionName + !emotionId + || !emotionName || emotionCount <= 0 ) { return; } - // 최초 색상 그룹 생성 + // 색상 그룹 최초 생성 if (!groupedData[colorGroup]) { groupedData[colorGroup] = { - colorGroup: colorGroup, + colorGroup, count: 0, + emotionIds: [], emotionNames: [], }; } @@ -778,7 +901,23 @@ document.addEventListener("DOMContentLoaded", () => { emotionCount; - // 감정 이름 중복 제거 후 추가 + // 감정 ID 추가 + if ( + !groupedData[ + colorGroup + ].emotionIds.includes( + emotionId + ) + ) { + groupedData[ + colorGroup + ].emotionIds.push( + emotionId + ); + } + + + // 감정 이름 추가 if ( !groupedData[ colorGroup @@ -796,7 +935,6 @@ document.addEventListener("DOMContentLoaded", () => { ); - // 객체를 배열로 변환 return Object.values( groupedData ); @@ -804,7 +942,7 @@ document.addEventListener("DOMContentLoaded", () => { /* ========================================================= - 22. 감정 그래프 툴팁 표시 + 25. 도넛 그래프 툴팁 표시 ========================================================= */ function showEmotionChartTooltip( @@ -821,7 +959,7 @@ document.addEventListener("DOMContentLoaded", () => { } - // 감정 이름만 표시 + // 감정 이름 표시 emotionChartTooltip.textContent = emotionNames.join(" · "); @@ -830,12 +968,10 @@ document.addEventListener("DOMContentLoaded", () => { emotionChartTooltip.hidden = false; - // wrapper 위치 계산 const wrapperRect = emotionChartWrapper.getBoundingClientRect(); - // 마우스 기준 툴팁 위치 const tooltipX = event.clientX - wrapperRect.left; @@ -845,39 +981,37 @@ document.addEventListener("DOMContentLoaded", () => { - wrapperRect.top; - // 마우스보다 약간 위쪽에 배치 + // 마우스 위쪽 배치 emotionChartTooltip.style.left = `${tooltipX}px`; emotionChartTooltip.style.top = - `${tooltipY - 16}px`; + `${tooltipY - 18}px`; } /* ========================================================= - 23. 감정 그래프 툴팁 숨기기 + 26. 도넛 그래프 툴팁 제거 ========================================================= */ function hideEmotionChartTooltip() { - // 툴팁 없는 경우 종료 if (!emotionChartTooltip) { return; } - // 툴팁 숨김 emotionChartTooltip.hidden = true; } /* ========================================================= - 24. 감정 SVG 도넛 그래프 생성 + 27. SVG 도넛 그래프 생성 ========================================================= */ function renderEmotionChart() { - // 그래프 요소 없는 경우 종료 + // 그래프 없는 경우 종료 if ( !emotionChart || !emotionChartSegments @@ -886,22 +1020,21 @@ document.addEventListener("DOMContentLoaded", () => { } - // 기존 그래프 조각 제거 + // 기존 조각 제거 emotionChartSegments.innerHTML = ""; - // 색상 그룹별 데이터 생성 const chartGroups = getEmotionChartGroups(); - // 기록 없는 경우 종료 + // 데이터 없는 경우 종료 if (chartGroups.length === 0) { return; } - // 전체 대표 감정 기록 수 계산 + // 전체 기록 수 계산 const totalCount = chartGroups.reduce( (sum, group) => @@ -910,26 +1043,18 @@ document.addEventListener("DOMContentLoaded", () => { ); - // 전체 기록 없는 경우 종료 if (totalCount <= 0) { return; } - /* - SVG 원 정보 - - emotion_list.html: - cx = 160 - cy = 160 - r = 110 - */ + // SVG 원 정보 const centerX = 160; const centerY = 160; - const radius = 110; + const radius = 112; - // 원 둘레 계산 + // 전체 원 둘레 const circumference = 2 * Math.PI * radius; @@ -938,44 +1063,33 @@ document.addEventListener("DOMContentLoaded", () => { 조각 사이 간격 값 증가 - → 조각 사이 간격 증가 - - 값 감소 - → 조각 사이 간격 감소 + → 조각 사이 흰색 간격 증가 */ - const segmentGap = 7; + const segmentGap = 5; /* - 시작 위치 + 12시 방향 시작 - SVG 원은 기본적으로 오른쪽에서 시작함. - - -circumference / 4 적용 - → 그래프 시작점을 12시 방향으로 이동 + 기본 circle 시작점은 오른쪽이므로 + rotate(-90deg) 적용 */ - let currentOffset = - -circumference / 4; + let currentLength = 0; chartGroups.forEach((group) => { - // 그룹 비율 계산 const ratio = - group.count / totalCount; + group.count + / totalCount; - // 그룹이 차지하는 원 둘레 길이 + // 실제 그룹 길이 const segmentLength = circumference * ratio; - /* - 실제 표시 길이에서 간격 제거 - - 너무 작은 조각이 사라지는 문제 방지용 - 최소 길이 1 적용 - */ + // 흰색 간격 제외 const visibleLength = Math.max( segmentLength @@ -984,7 +1098,6 @@ document.addEventListener("DOMContentLoaded", () => { ); - // SVG circle 생성 const segment = document.createElementNS( "http://www.w3.org/2000/svg", @@ -992,7 +1105,6 @@ document.addEventListener("DOMContentLoaded", () => { ); - // 기본 원 위치 설정 segment.setAttribute( "cx", centerX @@ -1009,60 +1121,47 @@ document.addEventListener("DOMContentLoaded", () => { ); - // CSS 클래스 추가 segment.classList.add( "emotion-chart-segment" ); - // 색상 그룹 클래스 추가 segment.classList.add( `emotion-chart-segment--${group.colorGroup}` ); - // 실제 stroke 색상 적용 - segment.style.stroke = - emotionChartColors[ - group.colorGroup - ] - || emotionChartColors.default; - - /* - 현재 조각 길이 설정 + 현재 조각만 표시 - 첫 번째 값 - → 실제 표시되는 선 길이 + visibleLength + → 보이는 부분 - 두 번째 값 - → 나머지 비어 있는 선 길이 + 나머지 + → 숨겨지는 부분 */ segment.style.strokeDasharray = `${visibleLength} ${ - circumference - - visibleLength + circumference - visibleLength }`; - // 현재 조각 시작 위치 설정 + /* + 조각 시작 위치 이동 + */ segment.style.strokeDashoffset = - `${-currentOffset}`; + `${-currentLength}`; - /* - hover에서 사용할 감정 이름 저장 + // hover 데이터 저장 + segment.dataset.emotionIds = + group.emotionIds.join(","); - 예: - "기쁨|만족|신남" - */ segment.dataset.emotionNames = group.emotionNames.join("|"); - /* - 마우스 진입 시 툴팁 표시 - */ + // hover 툴팁 표시 segment.addEventListener( "mouseenter", (event) => { @@ -1075,10 +1174,7 @@ document.addEventListener("DOMContentLoaded", () => { ); - /* - 조각 위에서 마우스 이동 시 - 툴팁 위치도 함께 이동 - */ + // 툴팁 위치 이동 segment.addEventListener( "mousemove", (event) => { @@ -1091,161 +1187,81 @@ document.addEventListener("DOMContentLoaded", () => { ); - /* - 마우스 이탈 시 툴팁 제거 - */ + // 툴팁 제거 segment.addEventListener( "mouseleave", hideEmotionChartTooltip ); - // SVG에 조각 추가 + /* + 색상 조각 클릭 + + 해당 그룹에 포함된 모든 대표 감정 기록 표시 + */ + segment.addEventListener( + "click", + () => { + + hideEmotionChartTooltip(); + + + openEmotionModal( + group.emotionIds, + group.emotionNames + ); + } + ); + + emotionChartSegments.appendChild( segment ); // 다음 조각 시작 위치 계산 - currentOffset += + currentLength += segmentLength; }); } /* ========================================================= - 25. 감정 도넛 그래프 초기 생성 + 28. 도넛 그래프 생성 ========================================================= */ renderEmotionChart(); /* ========================================================= - 26. 감정별 기록 팝업 요소 + 29. 감정 카드 클릭 ========================================================= */ - /* - 기존 emotion_modal.html 기능 유지. - - 현재 도넛 그래프에서는 - 감정 클릭 기능을 연결하지 않음. - - 추후 그래프 클릭 또는 별도 감정 버튼 추가 시 - openEmotionModal() 재사용 가능. - */ - - const modal = - document.querySelector( - "#emotion-modal" - ); - - const modalName = - document.querySelector( - "#emotion-modal-name" - ); - - const modalRecords = + const emotionRecordCards = document.querySelectorAll( - ".emotion-modal-record" - ); - - const modalEmpty = - document.querySelector( - "#emotion-modal-empty" - ); - - const closeButtons = - document.querySelectorAll( - "[data-modal-close]" - ); - - - /* ========================================================= - 27. 감정별 기록 팝업 열기 - ========================================================= */ - - function openEmotionModal( - emotionId, - emotionName - ) { - - // 팝업 요소 없는 경우 종료 - if ( - !modal - || !modalName - || !modalEmpty - ) { - return; - } - - - let visibleRecordCount = 0; - - - // 팝업 감정 이름 설정 - modalName.textContent = - emotionName; - - - // 선택 감정 기록만 표시 - modalRecords.forEach((record) => { - - const isMatched = - record.dataset.recordEmotion - === emotionId; - - - record.hidden = - !isMatched; - - - if (isMatched) { - visibleRecordCount += 1; - } - }); - - - // 기록 없음 문구 처리 - modalEmpty.hidden = - visibleRecordCount !== 0; - - - // 팝업 표시 - modal.hidden = false; - - - // 배경 스크롤 차단 - document.body.classList.add( - "modal-open" + "[data-emotion-card]" ); - } - /* ========================================================= - 28. 감정별 기록 팝업 닫기 - ========================================================= */ + emotionRecordCards.forEach( + (card) => { - function closeEmotionModal() { + card.addEventListener( + "click", + () => { - // 팝업 없는 경우 종료 - if (!modal) { - return; + openEmotionModal( + card.dataset.emotionId, + card.dataset.emotionName + ); + } + ); } - - - // 팝업 숨김 - modal.hidden = true; - - - // 배경 스크롤 복구 - document.body.classList.remove( - "modal-open" - ); - } + ); /* ========================================================= - 29. 감정 팝업 닫기 + 30. 감정 팝업 닫기 ========================================================= */ closeButtons.forEach((button) => { @@ -1256,68 +1272,4 @@ document.addEventListener("DOMContentLoaded", () => { ); }); - - /* ========================================================= - 30. Escape 키 처리 - ========================================================= */ - - document.addEventListener( - "keydown", - (event) => { - - // Escape 아닌 경우 종료 - if (event.key !== "Escape") { - return; - } - - - // 감정 팝업 닫기 - if ( - modal - && !modal.hidden - ) { - closeEmotionModal(); - } - - - // 장소 팝업 닫기 - if ( - locationModal - && !locationModal.hidden - ) { - closeLocationModal(); - } - - - // 지도 설정 메뉴 닫기 - if ( - mapSettingsMenu - && !mapSettingsMenu.hidden - ) { - mapSettingsMenu.hidden = true; - - - if (mapSettingsButton) { - - mapSettingsButton.setAttribute( - "aria-expanded", - "false" - ); - } - } - - - // 기간 설정창 닫기 - if ( - periodFilter - && !periodFilter.hidden - ) { - closePeriodFilter(); - } - - - // 감정 툴팁 제거 - hideEmotionChartTooltip(); - } - ); -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/diary/templates/diary/partials/emotion_list.html b/diary/templates/diary/partials/emotion_list.html index 3c956d3..f3d025e 100644 --- a/diary/templates/diary/partials/emotion_list.html +++ b/diary/templates/diary/partials/emotion_list.html @@ -1,27 +1,23 @@ +{% load static %} + {% comment %} 감정별 기록 탭 -구성: -1. 감정별 기록 제목 -2. SVG 도넛 그래프 -3. 가운데 전체 기록 수 -4. 그래프 영역 hover 시 감정 이름 툴팁 -5. JavaScript가 읽을 대표 감정 데이터 +구성 +- 대표 감정 도넛 그래프 +- 전체 기록 수 +- 색상 그룹 hover 툴팁 +- 기록된 대표 감정 카드 {% endcomment %} -
- +
-
-

감정별 기록

@@ -29,127 +25,150 @@

기록한 대표 감정을 한눈에 확인해보세요.

-

-
- -
+ +
- -
+ +
- - - - - - - - - - - - - -
- - - 전체 기록 - - - - {{ total_record_count }}개 - - + + + + + + + + + + + + + +
+ + 전체 기록 + + + + {{ total_record_count }}개 + +
+ + + +
+
- - + {% if recorded_emotions %} +
+ - 예: - 행복 · 설렘 + + ♥ + - 횟수는 표시 X + + 다른 감정들도 살펴볼까요? + - 위치와 내용은 diary.js에서 변경함 - --> - + + ♥ + -
+ +
+ {% endif %} + {% if recorded_emotions %} +
+ + {% for emotion in recorded_emotions %} + + + + {% endfor %} - + - + {% if not recorded_emotions %} -
-

아직 기록된 감정이 없어요.

@@ -207,9 +196,7 @@

오늘의 대표 감정을 기록해보세요.

-

- {% endif %}
From f5f9656df41ec07a417bfe4386ec50f5d2ee2a49 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 19:40:14 +0900 Subject: [PATCH 08/13] =?UTF-8?q?style:=20=EB=94=94=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9C=84=EC=95=84=EB=9E=98=20?= =?UTF-8?q?=EC=97=AC=EB=B0=B1=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/static/diary/css/detail.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diary/static/diary/css/detail.css b/diary/static/diary/css/detail.css index b1ca107..25fad0a 100644 --- a/diary/static/diary/css/detail.css +++ b/diary/static/diary/css/detail.css @@ -37,7 +37,7 @@ 첫 번째 숫자를 키우면: → 콘텐츠가 아래로 내려갑니다. */ - padding: 100px 0 100px; + padding: 20px 0 20px; /* width 안에 padding과 border까지 포함하도록 합니다. From c4403ad677ca8857a6370618ae5726734b41dd38 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 19:54:19 +0900 Subject: [PATCH 09/13] =?UTF-8?q?style:=20=EC=9E=A5=EC=86=8C=EB=B3=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B2=84=ED=8A=BC=20=ED=83=AD=20=EC=9A=B0?= =?UTF-8?q?=EC=B8=A1=20=EB=B0=B0=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/static/diary/css/diary.css | 65 +++++++++++-- diary/templates/diary/list.html | 154 ++++++++++++++++--------------- 2 files changed, 135 insertions(+), 84 deletions(-) diff --git a/diary/static/diary/css/diary.css b/diary/static/diary/css/diary.css index 49bec31..98a1ae1 100644 --- a/diary/static/diary/css/diary.css +++ b/diary/static/diary/css/diary.css @@ -18,9 +18,16 @@ 2. 전체 / 감정별 / 장소별 탭 ========================================================= */ -.diary-tabs-wrapper { - /* 탭 중앙 배치 */ +.diary-tabs-row { + /* + 탭과 점 세 개 버튼을 같은 줄에 배치 + 탭은 화면 중앙 유지 + 점 세 개 버튼의 위치 기준 역할 + */ + position: relative; + display: flex; + align-items: center; justify-content: center; width: 100%; @@ -30,6 +37,19 @@ } +.diary-tabs-wrapper { + /* + 탭 자체만 중앙 배치 + width: 100% 제거하여 오른쪽 버튼이 탭 위치에 영향 주지 않도록 함 + */ + display: flex; + justify-content: center; + + width: auto; + margin-bottom: 0; +} + + .diary-tabs { /* 탭 한 줄 배치 */ display: inline-flex; @@ -1231,22 +1251,20 @@ body.modal-open { ========================================================= */ .location-map-section { - /* 툴바와 지도 세로 배치 */ + /* 지도만 세로 배치 */ display: flex; flex-direction: column; - gap: 10px; width: 100%; } +/* + 기존 지도 위 별도 툴바는 사용하지 않음 + list.html에서 .map-toolbar 영역을 제거한 상태 기준 +*/ .map-toolbar { - /* 설정 버튼 오른쪽 배치 */ - display: flex; - align-items: center; - justify-content: flex-end; - - width: 100%; + display: none; } @@ -1257,6 +1275,20 @@ body.modal-open { } +/* + 탭 행 안에 있는 지도 설정 버튼 영역 + 탭은 가운데 유지하고 설정 버튼만 오른쪽 끝에 고정 +*/ +.diary-tabs-row > .map-display-settings { + position: absolute; + + top: 50%; + right: 0; + + transform: translateY(-50%); +} + + .map-settings-button { display: flex; align-items: center; @@ -1587,6 +1619,19 @@ body.modal-open { } + + .diary-tabs-row > .map-display-settings { + right: 0; + } + + + .diary-tabs-row .map-settings-button { + width: 38px; + height: 38px; + + font-size: 22px; + } + .diary-list-header { /* 제목과 필터 세로 배치 */ align-items: flex-start; diff --git a/diary/templates/diary/list.html b/diary/templates/diary/list.html index 5cc23e0..44b11e5 100644 --- a/diary/templates/diary/list.html +++ b/diary/templates/diary/list.html @@ -35,8 +35,86 @@ -
- {% include "diary/partials/diary_tab.html" %} + +
+ + +
+ {% include "diary/partials/diary_tab.html" %} +
+ + + + {% if selected_tab == "location" %} +
+ + + + + + + + +
+ {% endif %} +
@@ -320,78 +398,6 @@

- -
- -
- - - - - - - -
-
- -
From 743d334a0407c70d67fe65e262a17c812fdf78eb Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 20:04:57 +0900 Subject: [PATCH 10/13] =?UTF-8?q?style:=20=EC=9E=A5=EC=86=8C=EB=B3=84=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=9C=84=EC=B9=98=20=EC=83=81=ED=96=A5=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/static/diary/css/diary.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/diary/static/diary/css/diary.css b/diary/static/diary/css/diary.css index 98a1ae1..82f403e 100644 --- a/diary/static/diary/css/diary.css +++ b/diary/static/diary/css/diary.css @@ -1407,7 +1407,9 @@ body.modal-open { width: min(800px, 100%); - margin: 0 auto; + margin: -50px auto -100px; + + overflow: hidden; } From 4079776ae40101fd770a7e0edda37ba0570fc506 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 20:13:30 +0900 Subject: [PATCH 11/13] =?UTF-8?q?fix:=20=EC=A7=80=EB=8F=84=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=A1=B0=EC=A0=95=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=B4=20=ED=83=AD=EB=B0=94=20=EC=95=88=EB=88=8C=EB=A6=AC?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/static/diary/css/diary.css | 2 +- diary/templates/diary/partials/seoul_map.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/diary/static/diary/css/diary.css b/diary/static/diary/css/diary.css index 82f403e..40a2640 100644 --- a/diary/static/diary/css/diary.css +++ b/diary/static/diary/css/diary.css @@ -1407,7 +1407,7 @@ body.modal-open { width: min(800px, 100%); - margin: -50px auto -100px; + margin: 0 auto; overflow: hidden; } diff --git a/diary/templates/diary/partials/seoul_map.html b/diary/templates/diary/partials/seoul_map.html index 51812ca..45f1250 100644 --- a/diary/templates/diary/partials/seoul_map.html +++ b/diary/templates/diary/partials/seoul_map.html @@ -1,6 +1,6 @@ From 05603b21c00a770be267ddd74d26906fcaf96c14 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 20:17:53 +0900 Subject: [PATCH 12/13] =?UTF-8?q?style:=20=EA=B0=90=EC=A0=95=EB=B3=84=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=EB=B0=98=EC=9D=91=ED=98=95=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/static/diary/css/diary.css | 420 +++++++++++++++++++++++++++++-- 1 file changed, 398 insertions(+), 22 deletions(-) diff --git a/diary/static/diary/css/diary.css b/diary/static/diary/css/diary.css index 40a2640..32e8ebf 100644 --- a/diary/static/diary/css/diary.css +++ b/diary/static/diary/css/diary.css @@ -1584,20 +1584,30 @@ body.modal-open { 24. 반응형 스타일 ========================================================= */ -/* 1200px 이하 */ + +/* ========================================================= + 1200px 이하 + ========================================================= */ + @media (max-width: 1200px) { - /* 카드 한 줄 4개 */ + /* 전체 기록 카드 한 줄 4개 */ .diary-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } } -/* 900px 이하 */ +/* ========================================================= + 900px 이하 + ========================================================= */ + @media (max-width: 900px) { - /* 카드 및 팝업 한 줄 3개 */ + /* + 전체 기록 카드와 팝업 카드 + 한 줄에 3개 배치 + */ .diary-grid, .emotion-modal-grid, .location-modal-grid { @@ -1605,22 +1615,62 @@ body.modal-open { } - /* 감정 그래프 크기 축소 */ + /* + 감정 도넛 그래프 크기 축소 + + 기존 360px보다 조금 작게 조정 + 화면이 줄어들기 시작하는 구간에서 + 그래프가 과하게 커 보이지 않도록 함 + */ .emotion-chart-wrapper { - width: min(420px, 100%); + width: min(340px, 100%); + } + + + /* + 감정 카드가 너무 길게 한 줄로 늘어나는 것 방지 + + 데스크톱에서는 flex이지만 + 900px 이하부터 grid로 변경 + */ + .emotion-record-card-list { + display: grid; + + grid-template-columns: + repeat(3, minmax(0, 1fr)); + + gap: 12px; + } + + + /* + 기존 min-width: 150px 제거 + + grid 칸 너비에 맞춰 + 카드가 자연스럽게 줄어들도록 함 + */ + .emotion-record-card { + min-width: 0; + width: 100%; } } -/* 760px 이하 */ +/* ========================================================= + 760px 이하 + ========================================================= */ + @media (max-width: 760px) { + /* 페이지 좌우 여백 축소 */ .diary-list-container { - /* 좌우 여백 축소 */ width: calc(100% - 40px); } + /* ----------------------------------------------------- + 상단 탭 / 지도 설정 버튼 + ----------------------------------------------------- */ .diary-tabs-row > .map-display-settings { right: 0; @@ -1634,10 +1684,19 @@ body.modal-open { font-size: 22px; } + + /* ----------------------------------------------------- + 전체 기록 헤더 + ----------------------------------------------------- */ + .diary-list-header { - /* 제목과 필터 세로 배치 */ + /* + 제목과 필터를 + 세로 방향으로 변경 + */ align-items: flex-start; flex-direction: column; + gap: 14px; } @@ -1648,8 +1707,15 @@ body.modal-open { } + /* ----------------------------------------------------- + 기간 설정 팝업 + ----------------------------------------------------- */ + .period-filter { - /* 기간 설정창 아래 배치 */ + /* + 데스크톱에서는 오른쪽에 열리지만 + 작은 화면에서는 아래쪽에 표시 + */ top: calc(100% + 10px); left: 0; @@ -1664,32 +1730,133 @@ body.modal-open { } + /* ----------------------------------------------------- + 감정별 화면 + ----------------------------------------------------- */ + .emotion-page-header { - /* 감정 제목 세로 배치 */ + /* + 제목 영역 세로 배치 + */ flex-direction: column; + + gap: 10px; + } + + + .emotion-summary-board { + /* + 감정별 전체 보드의 + 좌우 여백 축소 + */ + padding: + 24px + 18px + 22px; + + border-radius: 22px; } .emotion-chart-section { - /* 모바일 그래프 높이 축소 */ - min-height: 450px; + /* + 기존 min-height: 450px 제거 + + 모바일에서 도넛 아래에 + 불필요한 빈 공간이 생기지 않도록 함 + */ + min-height: 300px; } .emotion-chart-wrapper { - width: min(400px, 100%); + /* + 화면 너비에 맞춰 + 도넛 그래프 자동 축소 + */ + width: min(300px, 78vw); + } + + + .emotion-chart-center-count { + font-size: 30px; + } + + + .emotion-chart-center-label { + font-size: 13px; + } + + + /* + 감정 카드 + 한 줄 2개 배치 + */ + .emotion-record-card-list { + display: grid; + + grid-template-columns: + repeat(2, minmax(0, 1fr)); + + gap: 12px; + + width: 100%; + } + + + .emotion-record-card { + /* + 기존 최소 너비 제거 + + 화면 크기에 따라 + 카드가 자연스럽게 축소됨 + */ + min-width: 0; + width: 100%; + + padding: + 10px + 12px; + } + + + .emotion-record-card-image { + width: 42px; + height: 42px; + } + + + .emotion-record-card-image img { + width: 38px; + height: 38px; + } + + + .emotion-card-guide { + margin-bottom: 16px; } } -/* 650px 이하 */ +/* ========================================================= + 650px 이하 + ========================================================= */ + @media (max-width: 650px) { + /* ----------------------------------------------------- + 전체 기록 카드 + ----------------------------------------------------- */ + .diary-grid, .emotion-modal-grid, .location-modal-grid { - /* 카드 및 팝업 한 줄 2개 */ - grid-template-columns: repeat(2, minmax(0, 1fr)); + /* + 전체 기록 및 팝업 카드는 + 한 줄 2개 + */ + grid-template-columns: + repeat(2, minmax(0, 1fr)); gap: 14px; } @@ -1697,6 +1864,7 @@ body.modal-open { .diary-card-content { min-height: 118px; + padding: 12px; } @@ -1706,19 +1874,36 @@ body.modal-open { } + /* ----------------------------------------------------- + 감정별 도넛 그래프 + ----------------------------------------------------- */ + .emotion-chart-section { - min-height: 390px; + /* + 760px 구간보다 + 조금 더 작게 축소 + */ + min-height: 270px; } .emotion-chart-wrapper { - width: min(350px, 100%); + /* + 기존 350px 제거 + + 760px에서 300px로 줄였는데 + 다시 350px이 되면서 커지던 문제 수정 + */ + width: min(270px, 76vw); } .emotion-chart-background, .emotion-chart-segment { - /* 작은 화면에서 도넛 두께 축소 */ + /* + 작은 화면에서 + 도넛 띠 두께 축소 + */ stroke-width: 40; } @@ -1729,20 +1914,211 @@ body.modal-open { .emotion-chart-center-count { - /* 가운데 기록 수 크기 축소 */ - font-size: 30px; + font-size: 27px; } .emotion-chart-center-label { + font-size: 12px; + } + + + /* ----------------------------------------------------- + 감정별 카드 + ----------------------------------------------------- */ + + .emotion-summary-board { + padding: + 20px + 14px + 18px; + + border-radius: 18px; + } + + + /* + 650px 이하에서도 + 일단 두 줄 배치 유지 + + 너무 빨리 한 줄 하나로 바뀌지 않도록 함 + */ + .emotion-record-card-list { + grid-template-columns: + repeat(2, minmax(0, 1fr)); + + gap: 10px; + } + + + .emotion-record-card { + gap: 8px; + + min-height: 62px; + + padding: + 9px + 10px; + } + + + .emotion-record-card-image { + width: 38px; + height: 38px; + } + + + .emotion-record-card-image img { + width: 35px; + height: 35px; + } + + + .emotion-record-card-name { font-size: 13px; } + .emotion-record-card-count { + font-size: 12px; + } + + + /* ----------------------------------------------------- + 팝업 + ----------------------------------------------------- */ + .emotion-modal-content, .location-modal-content { width: calc(100% - 28px); padding: 20px; } +} + + +/* ========================================================= + 480px 이하 + ========================================================= */ + +@media (max-width: 480px) { + + /* 페이지 좌우 여백 추가 축소 */ + .diary-list-container { + width: calc(100% - 24px); + } + + + /* ----------------------------------------------------- + 감정별 도넛 그래프 + ----------------------------------------------------- */ + + .emotion-chart-section { + min-height: 240px; + } + + + .emotion-chart-wrapper { + width: min(235px, 72vw); + } + + + .emotion-chart-background, + .emotion-chart-segment { + /* + 아주 작은 화면에서 + 도넛 띠 추가 축소 + */ + stroke-width: 35; + } + + + .emotion-chart-segment:hover { + stroke-width: 38; + } + + + .emotion-chart-center-count { + font-size: 24px; + } + + + .emotion-chart-center-label { + font-size: 11px; + } + + + /* ----------------------------------------------------- + 감정별 보드 + ----------------------------------------------------- */ + + .emotion-summary-board { + padding: + 18px + 12px + 16px; + + border-radius: 16px; + } + + + .emotion-card-guide { + gap: 6px; + + margin-bottom: 14px; + + font-size: 11px; + } + + + .emotion-card-guide-line { + width: 30px; + } + + + /* + 아주 작은 모바일에서는 + 감정 카드 한 줄 하나씩 배치 + */ + .emotion-record-card-list { + grid-template-columns: 1fr; + + gap: 9px; + } + + + .emotion-record-card { + min-height: 58px; + + padding: + 8px + 12px; + } + + + .emotion-record-card-image { + width: 40px; + height: 40px; + } + + + .emotion-record-card-image img { + width: 36px; + height: 36px; + } + + + /* ----------------------------------------------------- + 전체 기록 카드 + ----------------------------------------------------- */ + + .diary-grid, + .emotion-modal-grid, + .location-modal-grid { + /* + 아주 작은 화면에서는 + 기록 카드도 한 줄 하나씩 표시 + */ + grid-template-columns: 1fr; + } } \ No newline at end of file From 0badd916a54ca8f620811b33247fe53f4d18e317 Mon Sep 17 00:00:00 2001 From: 1wltn <1jisoo0120@gmail.com> Date: Fri, 7 Aug 2026 20:24:46 +0900 Subject: [PATCH 13/13] =?UTF-8?q?fix:=20=EA=B0=90=EC=A0=95=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=A0=95=EC=A0=81=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diary/templates/diary/partials/emotion_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diary/templates/diary/partials/emotion_list.html b/diary/templates/diary/partials/emotion_list.html index f3d025e..94cd076 100644 --- a/diary/templates/diary/partials/emotion_list.html +++ b/diary/templates/diary/partials/emotion_list.html @@ -141,7 +141,7 @@

{{ emotion.name }}