Skip to content

Commit 62b1aea

Browse files
authored
Merge pull request #75 from pirogramming/refactor/image-change
[feat] tour로 첫 회원가입시 가이드
2 parents b40503d + c2e8a85 commit 62b1aea

8 files changed

Lines changed: 302 additions & 22 deletions

File tree

‎static/css/allnight_mode.css‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ body {
3636

3737
.logo {
3838
position: relative;
39+
margin-top: 24px;
3940
}
4041

4142
.logotext {
43+
4244
display: flex;
4345
flex-direction: row;
4446
align-items: center;
@@ -47,17 +49,21 @@ body {
4749
}
4850

4951
.logotext__icon {
50-
width: 36px;
52+
width: 50px;
5153
height: auto;
5254
}
5355

5456
.logotext__words {
55-
display: flex;
56-
flex-direction: column;
57+
position: relative;
58+
display: inline-block;
5759
color: #F4EFE9;
58-
font-family: var(--cd-font-display);
59-
font-size: 24px;
60-
font-weight: bold;
60+
text-align: left;
61+
font-family: "Kirang Haerang";
62+
font-size: 30px;
63+
font-style: normal;
64+
font-weight: 400;
65+
line-height: 24px; /* 80% */
66+
padding: 10px;
6167
}
6268

6369
nav {
@@ -233,8 +239,8 @@ main {
233239
grid-template-columns: auto auto;
234240
justify-content: center;
235241
align-items: start; /* 회차 블록이 (음료 3종 세로 나열로) 길어져도 시각/본문이 각 행 위쪽에서 나란히 시작하도록 */
236-
column-gap: 13em;
237-
font-size: 20px;
242+
column-gap: 20em;
243+
font-size: 18px;
238244
row-gap: 28px; /* 회차 블록이 여러 줄이 되면서 13px로는 회차끼리 붙어 보여 넓혔다 */
239245
overflow: hidden;
240246
max-height: 2000px; /* 회차 블록이 길어진 만큼(기존 500px로는 회차가 많으면 잘림) 여유를 크게 잡음 */
@@ -379,7 +385,7 @@ main {
379385
}
380386

381387
.toggle_text {
382-
font-size: 16px;
388+
font-size: 15px;
383389
padding: 6px 18px;
384390
}
385391

@@ -391,7 +397,7 @@ main {
391397
column-gap: 0;
392398
justify-content: start;
393399
padding-left: 10%;
394-
font-size: 15px;
400+
font-size: 13px;
395401
}
396402

397403
.schedule_list.collapsed {

‎static/css/tour.css‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/* ==========================================================
2+
tour.css — 최초 접속 시 nav바를 하나씩 설명하는 온보딩 투어
3+
.cd-tour-spotlight는 실제로 "구멍 뚫린 배경"이 아니라, box-shadow를
4+
화면 전체 크기(9999px)로 깔아서 대상 영역만 빼고 전부 어둡게 보이게
5+
하는 흔한 트릭이다 — 대상이 바뀔 때마다 위치/크기만 옮기면 된다.
6+
========================================================== */
7+
8+
.cd-tour-spotlight {
9+
position: fixed;
10+
z-index: 201;
11+
border-radius: 12px;
12+
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.6);
13+
transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease;
14+
pointer-events: none; /* 시각 효과일 뿐 클릭은 못 받게 */
15+
}
16+
17+
/* 화면 전체를 덮어서, 투어 중엔 뒤에 있는 nav/콘텐츠를 실수로 못 누르게 막는다.
18+
클릭하면(카드를 직접 안 눌러도) 다음 단계로 넘어가게 해서 "클릭해서 계속"과 맞춘다. */
19+
.cd-tour-catcher {
20+
position: fixed;
21+
inset: 0;
22+
z-index: 200;
23+
cursor: pointer;
24+
}
25+
26+
.cd-tour-card {
27+
position: fixed;
28+
z-index: 202;
29+
max-width: 320px;
30+
cursor: pointer;
31+
}
32+
33+
.cd-tour-card__skip {
34+
position: absolute;
35+
z-index: 1;
36+
top: -8px;
37+
right: -8px;
38+
width: 28px;
39+
height: 28px;
40+
border-radius: 50%;
41+
border: none;
42+
background: var(--cd-cream-soft, #F8F2E9);
43+
color: var(--cd-brown-deep, #3E312F);
44+
font-size: 18px;
45+
line-height: 1;
46+
cursor: pointer;
47+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
48+
}
49+
50+
.cd-tour-card__body {
51+
display: flex;
52+
align-items: flex-end;
53+
gap: 8px;
54+
}
55+
56+
.cd-tour-card__icon {
57+
width: 48px;
58+
height: 48px;
59+
flex-shrink: 0;
60+
}
61+
62+
/* 채팅 말풍선 모양 — 왼쪽 아래만 각을 남겨 아이콘 쪽을 가리키는 느낌 */
63+
.cd-tour-card__bubble {
64+
position: relative;
65+
background: var(--cd-cream-soft, #F8F2E9);
66+
border-radius: 20px 20px 20px 4px;
67+
padding: 16px 20px;
68+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
69+
}
70+
71+
.cd-tour-card__title {
72+
font-family: var(--cd-font-body, cursive);
73+
font-size: 16px;
74+
color: var(--cd-brown-deep, #3E312F);
75+
margin: 0 0 6px;
76+
}
77+
78+
.cd-tour-card__desc {
79+
font-size: 13px;
80+
color: var(--cd-brown-ink, #362A28);
81+
margin: 0 0 10px;
82+
line-height: 1.4;
83+
}
84+
85+
.cd-tour-card__progress {
86+
font-size: 12px;
87+
font-weight: 600;
88+
color: #886f6b;
89+
margin: 0;
90+
}
91+
92+
93+
/* ---- 모바일: nav바가 햄버거 메뉴 안에 숨어있어 스포트라이트로 못 가리킨다.
94+
스포트라이트 없이, 카드에 "무슨 메뉴에 대한 설명인지" 제목만 보여주고
95+
화면 하단 중앙에 고정한다. ---- */
96+
@media (max-width: 480px) {
97+
.cd-tour-card--centered {
98+
top: auto !important;
99+
bottom: 40px !important;
100+
left: 50% !important;
101+
transform: translateX(-50%);
102+
width: calc(100% - 32px);
103+
max-width: 360px;
104+
}
105+
106+
.cd-tour-card--centered .cd-tour-card__body {
107+
flex-direction: column;
108+
align-items: center;
109+
text-align: center;
110+
}
111+
112+
.cd-tour-card__skip {
113+
top: 35px;
114+
right: -5px;
115+
}
116+
}
117+
118+
@media (prefers-reduced-motion: reduce) {
119+
.cd-tour-spotlight { transition: none; }
120+
}
121+

‎static/js/auth.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,13 @@ form?.addEventListener('submit', async (event) => {
7373
// 404(PROFILE_NOT_FOUND)를 받고 다시 튕겨나가므로 여기서 먼저 걸러낸다.
7474
// 기존 '/sleep-logs'는 페이지가 아니라 API 경로라 브라우저로 들어가면 깨진다.
7575
if (mode === 'signup') {
76+
// "회원가입 → feed 진입" 흐름이라는 표시를 남겨둔다. sleep-logs/time·rating을
77+
// 포함한 초기 설문 흐름을 다 거쳐 /feed/에 도착했을 때 온보딩 투어(tour.js)가
78+
// 뜨게 하려는 것 — daily_rating.js가 이 값을 보고 /feed/?first_visit=1로
79+
// 보낸 뒤 이 표시를 지운다.
80+
sessionStorage.setItem('cd_pending_tour', '1');
7681
window.location.href = '/initial_survey';
7782
} else {
7883
window.location.href = data.has_profile ? '/feed/' : '/initial_survey';
7984
}
80-
});
85+
});

‎static/js/daily_rating.js‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/**
2-
* daily_rating.js — 수면 평점 (SLEEP-002 / POST /sleep-logs)
3-
* API가 1~5만 허용하므로, 아무것도 안 고른 상태(0)로는 제출을 막는다.
4-
* 이전 화면(daily_time.html)에서 datetime-local로 받아 sessionStorage에 저장해둔 값을 그대로 꺼내 쓴다.
2+
* daily_rating.js — 수면 체크인 원두 평점 (SLEEP-002 / POST /sleep-logs)
3+
* 원두 5개를 1~5로 매핑한 라디오. API가 1~5만 허용하므로, 아무것도 안 고른 상태(0)로는 제출을 막는다.
54
*/
65

76
function getCookie(name) {
@@ -102,7 +101,13 @@ document.getElementById('cdDailyRatingForm')?.addEventListener('submit', async (
102101
// TODO: data.threshold_mg / personalization_weight 등을 화면에 반영하고 싶으면 여기서 사용
103102
sessionStorage.removeItem('cd_daily_actual_bedtime');
104103
sessionStorage.removeItem(RATING_STORAGE_KEY);
105-
window.location.href = '/feed/';
104+
105+
// 회원가입 직후 흐름으로 여기까지 온 경우에만(=auth.js가 남겨둔 표시가
106+
// 있을 때만) 온보딩 투어를 위한 쿼리를 붙인다. 평소 반복되는 수면설문
107+
// 완료 시엔 이 표시가 없어서 그냥 /feed/로만 이동한다.
108+
const isFirstSignupFlow = sessionStorage.getItem('cd_pending_tour') === '1';
109+
sessionStorage.removeItem('cd_pending_tour');
110+
window.location.href = isFirstSignupFlow ? '/feed/?first_visit=1' : '/feed/';
106111
return;
107112
}
108113

‎static/js/tour.js‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* tour.js — 회원가입 직후 feed로 넘어올 때 nav바를 하나씩 설명하는 온보딩 투어
3+
*
4+
* 실행 조건: URL에 ?first_visit=1 이 있으면 무조건 시작한다("최초 1회만"이
5+
* 아니라 회원가입 → feed 진입이라는 동작 자체에 붙는 연출). daily_rating.js가
6+
* 회원가입 직후 설문(sleep-logs/rating) 제출 성공 시 /feed/?first_visit=1
7+
* 로 보내는 것과 짝을 이룬다.
8+
*
9+
* nav바의 각 항목은 base.html에서 data-tour="..." 속성으로 표시해뒀다
10+
* (href가 바뀌어도 셀렉터가 안 깨지도록 href 대신 이 속성으로 찾는다).
11+
*/
12+
13+
(function () {
14+
const steps = [
15+
{ selector: '[data-tour="main"]', title: '메인 화면', desc: '마신 카페인 음료를 기록하면서 오늘의 카페인 마감 시간을 확인할 수 있어요.' },
16+
{ selector: '[data-tour="feed"]', title: '나의 다이어리', desc: '마신 카페인의 양, 수면 시간 등 지금까지 기록한 카페인 일지를 한눈에 볼 수 있어요.' },
17+
{ selector: '[data-tour="drinks"]', title: '음료 추가', desc: '즐겨 마시는 카페인 음료를 등록하고 수정할 수 있어요.' },
18+
{ selector: '[data-tour="allnight"]', title: '밤샘 모드', desc: '밤을 새울 계획이라면 여기에서 시간을 설정하고 계획을 추천받아 보세요.' },
19+
{ selector: '[data-tour="mypage"]', title: '마이페이지', desc: '내 수면 시각과 체중을 수정할 수 있어요.' },
20+
];
21+
22+
function shouldRunTour() {
23+
const params = new URLSearchParams(window.location.search);
24+
return params.get('first_visit') === '1';
25+
}
26+
27+
// 쿼리스트링을 지워서, 새로고침했을 때 투어가 다시 시작되지 않게 한다
28+
// (localStorage로도 막히지만, 주소창이 지저분해 보이는 것도 같이 정리).
29+
function cleanUrl() {
30+
const url = new URL(window.location.href);
31+
url.searchParams.delete('first_visit');
32+
window.history.replaceState({}, '', url.pathname + url.search);
33+
}
34+
35+
let currentIndex = 0;
36+
let els = {};
37+
let resizeHandler = null;
38+
39+
function buildDom() {
40+
const spotlight = document.createElement('div');
41+
spotlight.className = 'cd-tour-spotlight';
42+
43+
const catcher = document.createElement('div');
44+
catcher.className = 'cd-tour-catcher';
45+
46+
const card = document.createElement('div');
47+
card.className = 'cd-tour-card';
48+
card.innerHTML = `
49+
<div class="cd-tour-card__body">
50+
<img src="/static/images/dailysurvey-logo.svg" alt="" class="cd-tour-card__icon">
51+
<div class="cd-tour-card__bubble">
52+
<p class="cd-tour-card__title"></p>
53+
<p class="cd-tour-card__desc"></p>
54+
<p class="cd-tour-card__progress"></p>
55+
</div>
56+
</div>
57+
`;
58+
59+
document.body.appendChild(spotlight);
60+
document.body.appendChild(catcher);
61+
document.body.appendChild(card);
62+
63+
els = { spotlight, catcher, card };
64+
65+
card.addEventListener('click', advance);
66+
catcher.addEventListener('click', advance);
67+
68+
// 화면 크기가 바뀌면(반응형 레이아웃으로 nav 위치 자체가 이동하므로)
69+
// 현재 단계를 다시 그려서 스포트라이트/카드가 새 위치를 따라가게 한다.
70+
resizeHandler = () => renderStep();
71+
window.addEventListener('resize', resizeHandler);
72+
}
73+
74+
function renderStep() {
75+
const step = steps[currentIndex];
76+
const target = document.querySelector(step.selector);
77+
78+
els.card.querySelector('.cd-tour-card__title').textContent = step.title;
79+
els.card.querySelector('.cd-tour-card__desc').textContent = step.desc;
80+
els.card.querySelector('.cd-tour-card__progress').textContent =
81+
`${currentIndex + 1} / ${steps.length} · 클릭해서 계속`;
82+
83+
const isMobile = window.matchMedia('(max-width: 480px)').matches;
84+
85+
// 모바일이거나(햄버거 메뉴 안에 nav가 숨어있어 못 가리킴), 대상 요소를
86+
// 못 찾은 경우 스포트라이트 없이 카드만 화면 중앙 하단에 띄운다.
87+
if (!target || isMobile) {
88+
els.spotlight.style.display = 'none';
89+
els.card.classList.add('cd-tour-card--centered');
90+
return;
91+
}
92+
93+
els.card.classList.remove('cd-tour-card--centered');
94+
els.spotlight.style.display = 'block';
95+
96+
const rect = target.getBoundingClientRect();
97+
const pad = 8;
98+
els.spotlight.style.top = `${rect.top - pad}px`;
99+
els.spotlight.style.left = `${rect.left - pad}px`;
100+
els.spotlight.style.width = `${rect.width + pad * 2}px`;
101+
els.spotlight.style.height = `${rect.height + pad * 2}px`;
102+
103+
// 카드는 강조된 영역 바로 아래에 두되, 화면 아래쪽에 공간이 부족하면 위로
104+
const cardRect = els.card.getBoundingClientRect();
105+
const spaceBelow = window.innerHeight - rect.bottom;
106+
const top = spaceBelow > cardRect.height + 24
107+
? rect.bottom + 30
108+
: rect.top - cardRect.height - 16;
109+
110+
els.card.style.top = `${Math.max(16, top)}px`;
111+
els.card.style.left = `${Math.max(16, Math.min(rect.left, window.innerWidth - cardRect.width - 16))}px`;
112+
}
113+
114+
function advance() {
115+
currentIndex += 1;
116+
if (currentIndex >= steps.length) {
117+
endTour();
118+
return;
119+
}
120+
renderStep();
121+
}
122+
123+
function endTour() {
124+
if (resizeHandler) {
125+
window.removeEventListener('resize', resizeHandler);
126+
resizeHandler = null;
127+
}
128+
els.spotlight?.remove();
129+
els.catcher?.remove();
130+
els.card?.remove();
131+
}
132+
133+
function startTour() {
134+
if (!shouldRunTour()) return;
135+
buildDom();
136+
renderStep();
137+
cleanUrl();
138+
}
139+
140+
document.addEventListener('DOMContentLoaded', startTour);
141+
})();

‎templates/base.html‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@
2727
</div>
2828
</div>
2929
<nav class="cd-nav" aria-label="주 메뉴" id="cd-nav">
30-
<a href="{% url 'diary:feed' %}" class="cd-nav__link">메인 화면</a>
31-
<a href="{% url 'diary:mydiary' %}" class="cd-nav__link">나의 다이어리</a>
32-
<a href="{% url 'diary:favorites' %}" class="cd-nav__link">음료 추가</a>
33-
<a href="{% url 'diary:allnight-setup' %}" class="cd-nav__link">밤샘 모드</a>
30+
<a href="#" class="cd-nav__link" data-tour="main">메인 화면</a>
31+
<a href="{% url 'diary:feed' %}" class="cd-nav__link" data-tour="feed">나의 다이어리</a>
32+
<a href="{% url 'diary:drink-list' %}" class="cd-nav__link" data-tour="drinks">음료 추가</a>
33+
<a href="{% url 'diary:allnight-setup' %}" class="cd-nav__link" data-tour="allnight">밤샘 모드</a>
3434
<button type="button" class="cd-nav__link" id="cd-logout-btn">로그아웃</button>
35-
<a href="{% url 'diary:mypage' %}" class="cd-nav__link">마이페이지</a>
35+
<a href="{% url 'diary:mypage' %}" class="cd-nav__link" data-tour="mypage">마이페이지</a>
3636
</nav>
37-
37+
3838
<!-- 480px 미만에서만 보이는 햄버거 버튼. 평소엔 CSS로 숨겨져 있다. -->
3939
<button type="button" class="cd-nav-toggle" id="cd-nav-toggle" aria-label="메뉴 열기" aria-expanded="false" aria-controls="cd-nav">
4040
<span></span><span></span><span></span>

0 commit comments

Comments
 (0)