|
| 1 | +class StarWarsIntro extends HTMLElement { |
| 2 | + // 관찰할 속성 정의 |
| 3 | + static get observedAttributes() { |
| 4 | + return ["auto-play", "duration", "height"]; |
| 5 | + } |
| 6 | + |
| 7 | + constructor() { |
| 8 | + super(); |
| 9 | + this.attachShadow({ mode: "open" }); |
| 10 | + this.isPlaying = false; |
| 11 | + } |
| 12 | + |
| 13 | + connectedCallback() { |
| 14 | + this.render(); |
| 15 | + this.init(); |
| 16 | + |
| 17 | + const replayButton = this.shadowRoot.querySelector(".replay-button"); |
| 18 | + if (replayButton) { |
| 19 | + replayButton.addEventListener("click", () => { |
| 20 | + this.isPlaying = false; // Reset flag to allow restart |
| 21 | + this.startAnimation(); |
| 22 | + }); |
| 23 | + } |
| 24 | + } |
| 25 | + attributeChangedCallback(name, oldValue, newValue) { |
| 26 | + if (oldValue !== newValue) { |
| 27 | + this.render(); |
| 28 | + // 속성 변경 시 애니메이션을 재시작할 필요가 있다면 (예: duration 변경 시) |
| 29 | + if (name === "duration" && this.isPlaying) { |
| 30 | + this.startAnimation(); |
| 31 | + } |
| 32 | + } |
| 33 | + } |
| 34 | + |
| 35 | + init() { |
| 36 | + const autoPlay = this.getAttribute("auto-play") === "true"; |
| 37 | + |
| 38 | + if (autoPlay) { |
| 39 | + // 1초 후 자동 시작 |
| 40 | + setTimeout(() => this.startAnimation(), 1000); |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + /** |
| 45 | + * 애니메이션을 시작/재시작합니다. |
| 46 | + */ |
| 47 | + startAnimation() { |
| 48 | + if (this.isPlaying) return; // 이미 재생 중이라면 중복 실행 방지 |
| 49 | + |
| 50 | + this.isPlaying = true; |
| 51 | + const crawl = this.shadowRoot.querySelector(".starwars-crawl"); |
| 52 | + const replayButton = this.shadowRoot.querySelector(".replay-button"); // Get button reference |
| 53 | + |
| 54 | + // Hide button at start |
| 55 | + if (replayButton) { |
| 56 | + replayButton.style.display = "none"; |
| 57 | + } |
| 58 | + |
| 59 | + // 1. 애니메이션을 'none'으로 설정하여 초기 상태(0% 키프레임)로 리셋 |
| 60 | + // (top: 100%, opacity: 0) 상태로 되돌아갑니다. |
| 61 | + crawl.style.animation = "none"; |
| 62 | + |
| 63 | + // 2. DOM이 변경된 것을 감지할 수 있도록 짧은 딜레이 후 애니메이션 재시작 |
| 64 | + // (reflow/repaint를 유도하여 애니메이션을 처음부터 다시 재생) |
| 65 | + setTimeout(() => { |
| 66 | + crawl.style.animation = `starwarsCrawl ${this.duration}s linear`; |
| 67 | + }, 10); |
| 68 | + |
| 69 | + // 3. 애니메이션 종료 시간 계산 후 재시작 (무한 반복) |
| 70 | + setTimeout(() => { |
| 71 | + this.isPlaying = false; |
| 72 | + // Show button when animation ends |
| 73 | + if (replayButton) { |
| 74 | + replayButton.style.display = "block"; // Or 'inline-block' depending on desired layout |
| 75 | + } |
| 76 | + // auto-play가 true인 경우에만 재시작 |
| 77 | + if (this.getAttribute("auto-play") === "true") { |
| 78 | + this.startAnimation(); |
| 79 | + } |
| 80 | + }, this.duration * 1000); |
| 81 | + } |
| 82 | + |
| 83 | + get duration() { |
| 84 | + return parseInt(this.getAttribute("duration") || "40"); |
| 85 | + } |
| 86 | + |
| 87 | + get height() { |
| 88 | + return this.getAttribute("height") || "80vh"; |
| 89 | + } |
| 90 | + |
| 91 | + render() { |
| 92 | + this.shadowRoot.innerHTML = ` |
| 93 | + <style> |
| 94 | + :host { |
| 95 | + display: block; |
| 96 | + width: 100%; |
| 97 | + } |
| 98 | +
|
| 99 | + * { |
| 100 | + margin: 0; |
| 101 | + padding: 0; |
| 102 | + box-sizing: border-box; |
| 103 | + } |
| 104 | +
|
| 105 | + .starwars-container { |
| 106 | + position: relative; |
| 107 | + width: 100%; |
| 108 | + height: ${this.height}; |
| 109 | + min-height: 600px; |
| 110 | + background: transparent; |
| 111 | + overflow: hidden; /* 텍스트가 이 영역 밖으로 벗어나는 것을 숨김 */ |
| 112 | + border-radius: 3px; |
| 113 | + } |
| 114 | +
|
| 115 | +
|
| 116 | +
|
| 117 | + .starwars-text { |
| 118 | + display: flex; |
| 119 | + justify-content: center; |
| 120 | + position: absolute; |
| 121 | + width: 100%; |
| 122 | + height: 100%; |
| 123 | + top: 0; |
| 124 | + left: 0; |
| 125 | + color: #444; |
| 126 | + font-family: 'Pathway Gothic One', 'Pretendard', sans-serif; |
| 127 | + font-size: calc(2rem + 4vw); |
| 128 | + font-weight: 600; |
| 129 | + letter-spacing: 4px; |
| 130 | + line-height: 150%; |
| 131 | + perspective: 1200px; /* 3D 효과의 깊이 */ |
| 132 | + text-align: center; |
| 133 | + -webkit-mask-image: linear-gradient(to top, black 25%, transparent 100%); |
| 134 | + mask-image: linear-gradient(to top, black 25%, transparent 100%); |
| 135 | + } |
| 136 | +
|
| 137 | + .starwars-crawl { |
| 138 | + position: absolute; |
| 139 | + top: 100%; /* 초기 상태: 화면 아래쪽에 위치 */ |
| 140 | + transform-origin: 50% 100%; |
| 141 | + /* !!! 여기서 animation 속성을 제거했습니다. !!! */ |
| 142 | + /* !!! 이제 JS의 startAnimation()으로만 제어됩니다. !!! */ |
| 143 | + width: 200%; |
| 144 | +
|
| 145 | + } |
| 146 | +
|
| 147 | + .intro-title { |
| 148 | + text-align: center; |
| 149 | + margin-bottom: 3rem; |
| 150 | + } |
| 151 | +
|
| 152 | + .episode { |
| 153 | + font-size: 0.6em; |
| 154 | + margin: 0; |
| 155 | + opacity: 0.8; |
| 156 | + letter-spacing: 2px; |
| 157 | + } |
| 158 | +
|
| 159 | + .intro-title h2 { |
| 160 | + margin: 1rem 0 3rem; |
| 161 | + text-transform: uppercase; |
| 162 | + font-size: 1.1em; |
| 163 | + font-weight: 900; |
| 164 | + letter-spacing: 6px; |
| 165 | + color: #444; |
| 166 | + } |
| 167 | +
|
| 168 | + .intro-text { |
| 169 | + font-size: 0.55em; |
| 170 | + margin-bottom: 3rem; |
| 171 | + text-align: center; |
| 172 | + line-height: 140%; |
| 173 | + } |
| 174 | +
|
| 175 | + .intro-text.emphasis { |
| 176 | + font-size: 0.6em; |
| 177 | + margin: 4rem 0; |
| 178 | + font-weight: 700; |
| 179 | + } |
| 180 | +
|
| 181 | + @keyframes starwarsCrawl { |
| 182 | + 0% { |
| 183 | + top: 100%; /* 화면 아래쪽에서 시작 */ |
| 184 | + transform: rotateX(40deg) translateZ(0); |
| 185 | + opacity: 0; /* 투명한 상태에서 시작 */ |
| 186 | + } |
| 187 | + 10% { |
| 188 | + opacity: 1; |
| 189 | + } |
| 190 | + 100% { |
| 191 | + top: -1200%; /* 화면 위쪽으로 멀리 이동 */ |
| 192 | + transform: rotateX(55deg) translateZ(-3000px); |
| 193 | + opacity: 1; |
| 194 | + } |
| 195 | + } |
| 196 | +
|
| 197 | + .replay-button { |
| 198 | + position: absolute; |
| 199 | + bottom: 20px; |
| 200 | + right: 20px; |
| 201 | + padding: 10px 20px; |
| 202 | + background-color: #feda4a; |
| 203 | + color: black; |
| 204 | + border: none; |
| 205 | + border-radius: 5px; |
| 206 | + cursor: pointer; |
| 207 | + font-size: 1em; |
| 208 | + z-index: 10; /* Ensure it's above other elements */ |
| 209 | + display: none; /* Hidden by default */ |
| 210 | + } |
| 211 | +
|
| 212 | + .replay-button:hover { |
| 213 | + background-color: #e0c030; |
| 214 | + } |
| 215 | +
|
| 216 | + /* 컨트롤 버튼 (옵션) - 이 예시에서는 사용되지 않으나 스타일은 유지 */ |
| 217 | + .control-button { |
| 218 | + /* ... (스타일 유지) ... */ |
| 219 | + display: none; |
| 220 | + } |
| 221 | +
|
| 222 | + @media (max-width: 768px) { |
| 223 | + /* ... (미디어 쿼리 스타일 유지) ... */ |
| 224 | + } |
| 225 | + </style> |
| 226 | +
|
| 227 | +<div class="starwars-container"> |
| 228 | + <div class="starwars-text"> |
| 229 | + <div class="starwars-crawl"> |
| 230 | + <div class="intro-title"> |
| 231 | + <h2>KODINGWARRIOR QUEST</h2> |
| 232 | + </div> |
| 233 | +
|
| 234 | + <p class="intro-text"><i>한 개발자가 있습니다.</i><br/> |
| 235 | + <i>정해진 길을 걷지 않고,</i><br/> |
| 236 | + <i>자신만의 길을 고수하는 사람입니다.</i></p> |
| 237 | +
|
| 238 | + <p class="intro-text"><i>고행길인 걸 알면서도,</i><br/> |
| 239 | + <i>꿋꿋이 페이스를 유지하며,</i><br/> |
| 240 | + <i>조용히 자신의 일을 이어갑니다.</i></p> |
| 241 | +
|
| 242 | + <p class="intro-text">그는 화려한 기술보다<br/> |
| 243 | + 꾸준함이 더 어렵다는 걸 알고 있습니다.<br/> |
| 244 | + 그래서 오늘도 묵묵히 코드를 씁니다.</p> |
| 245 | +
|
| 246 | + <p class="intro-text">그는 Ruby로 시작해 Python, NestJS, Flutter로 이어갔습니다.<br/> |
| 247 | + 언어나 프레임워크는 중요하지 않습니다.<br/> |
| 248 | + 중요한 것은, 누군가에게 도움이 되는 결과입니다.</p> |
| 249 | +
|
| 250 | + <p class="intro-text emphasis">그의 하루는 단순합니다.<br/> |
| 251 | + 코드를 다듬고, 환경을 정비하며,<br/> |
| 252 | + 작은 불편을 발견하면 바로 고칩니다.<br/> |
| 253 | + 그렇게 조금씩, 제품은 더 나아집니다.</p> |
| 254 | +
|
| 255 | + <p class="intro-text">그는 익숙한 것보다<br/> |
| 256 | + 지금 필요한 것을 선택합니다.<br/> |
| 257 | + 새로운 기술은 낯설지만,<br/> |
| 258 | + 그 낯섦 속에서 배움을 즐깁니다.</p> |
| 259 | +
|
| 260 | + <p class="intro-text emphasis">그는 먼저 앞서가며,<br/> |
| 261 | + 뒤따르는 이들이 같은 길을 걷기 쉽게 만들고 싶어합니다.<br/> |
| 262 | + 그 길이 고행이 되지 않기를 바랍니다.</p> |
| 263 | +
|
| 264 | + <p class="intro-text">그는 완벽을 좇지 않습니다.<br/> |
| 265 | + 대신, 어제보다 나은 오늘을 선택합니다.<br/> |
| 266 | + 그게 프로라면, 그걸로 충분하다고 믿습니다.</p> |
| 267 | +
|
| 268 | + <p class="intro-text emphasis"><i>마이너한 스택을 다루는 개발자,</i><br/> |
| 269 | + <i>꾸준히 배우며 앞으로 나아가는 사람,</i><br/> |
| 270 | + <i>그의 여정은 오늘도 계속됩니다.</i></p> |
| 271 | + </div> |
| 272 | + </div> |
| 273 | + <button class="replay-button">Replay</button> |
| 274 | +</div> |
| 275 | +
|
| 276 | +
|
| 277 | +`; |
| 278 | + } |
| 279 | +} |
| 280 | + |
| 281 | +// Custom Element 등록 |
| 282 | +customElements.define("starwars-intro", StarWarsIntro); |
0 commit comments