wow
Some checks failed
/ print-content (push) Failing after 1m39s

This commit is contained in:
암냥 2026-09-07 00:09:32 +09:00
commit bcd4fa87b2
11 changed files with 679 additions and 34 deletions

View file

@ -1,6 +1,7 @@
---
import { events } from "@/lib/events";
import AboutRow from "@/components/AboutRow.astro";
import DDay from "./DDay.astro";
const sortedEvents = [...events].sort((a, b) => b.date.localeCompare(a.date));
const years = [...new Set(sortedEvents.map((event) => event.date.slice(0, 4)))];
@ -38,6 +39,26 @@ const daysUntilYearEnd = Math.ceil(
안녕하세요, 정보보안과 소프트웨어 엔지니어링을 주로하고 있는
암냥입니다.
</p>
<div class="flex flex-row mt-4 gap-1">
<b>생일 : </b><DDay targetDate="2010-11-08T03:00:00+09:00" />
</div>
<a href="#contact" class="flex flex-row items-center underline">
<span>연락하기</span>
<svg
aria-hidden="true"
class="h-4 text-foreground/40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 17 17 7"></path>
<path d="M7 7h10v10"></path>
</svg>
</a>
<br />
<p>저는...</p>
<section class="mt-[0.75em]">
<div class="mt-[0.5em] flex flex-col gap-[0.34em]">
@ -87,20 +108,40 @@ const daysUntilYearEnd = Math.ceil(
</AboutRow>
</div>
</section>
<p></p>
<div class="flex flex-row">
<a
href="#experience"
class="flex flex-row items-center underline"
><span>등등...</span>
<svg
aria-hidden="true"
class="h-4 text-foreground/40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 17 17 7"></path>
<path d="M7 7h10v10"></path>
</svg></a
>
<p>을 이루어냈어요.</p>
</div>
</div>
<div
class="relative order-first h-[min(150vw,720px)] min-h-[600px] overflow-hidden lg:sticky lg:top-0 lg:order-none lg:h-[calc(100svh-64px)] lg:min-h-0 lg:self-start"
aria-label="활동 사진"
>
<img
<!-- <img
class="absolute inset-0 h-full w-full select-none pointer-events-none object-contain"
src="/side.avif"
alt="활동 사진 콜라주"
width="3840"
height="4320"
/>
/> -->
</div>
</div>
</section>

View file

@ -0,0 +1,583 @@
---
// biome-ignore lint/correctness/noUnusedVariables: Used in the Astro template below.
const groups = [
{
id: "direct",
title: "연락처",
items: [
{
name: "개인 이메일",
value: "me@imnya.ng",
href: "mailto:me@imnya.ng",
icon: "mail",
tone: "pink",
email: true,
},
{
name: "dazzle.st 이메일",
value: "imnyang@dazzle.st",
href: "mailto:imnyang@dazzle.st",
icon: "mail",
tone: "green",
email: true,
},
{
name: "Discord",
value: "@imnyang",
href: "https://discord.com/users/909353223901569035",
icon: "discord",
tone: "purple",
},
],
},
{
id: "social",
title: "소셜",
items: [
{
name: "X (Twitter)",
value: "@imnya_ng",
href: "https://twitter.com/imnya_ng",
icon: "x",
tone: "blue",
},
{
name: "Instagram",
value: "@imnya.ng",
href: "https://instagram.com/imnya.ng",
icon: "instagram",
tone: "pink",
},
{
name: "YouTube",
value: "@imnyang",
href: "https://youtube.com/@imnyang",
icon: "youtube",
tone: "red",
},
],
},
{
id: "work",
title: "코드와 글",
items: [
{
name: "GitHub",
value: "@imnyang",
href: "https://github.com/imnyang",
icon: "github",
tone: "neutral",
},
{
name: "git.mizuki.guru",
value: "@imnyang",
href: "https://git.mizuki.guru/imnyang",
icon: "mizuki",
tone: "orange",
},
{
name: "Blog",
value: "blog.imnya.ng",
href: "https://blog.imnya.ng",
icon: "blog",
tone: "green",
},
],
},
{
id: "game",
title: "Game",
items: [
{
name: "Steam",
value: "@imnyang",
href: "https://steamcommunity.com/id/imnyang/",
icon: "steam",
tone: "blue",
},
{
name: "maimai",
value: "@imnya.ng",
href: "https://mai.sft.sh/imnyang",
icon: "maimai",
tone: "blue",
},
{
name: "Project Sekai KR",
value: "7354623251148069648",
href: "#",
icon: "project-sekai",
tone: "blue",
},
{
name: "Project Sekai JP",
value: "729310364018688008",
href: "#",
icon: "project-sekai",
tone: "blue",
},
{
name: "VRChat",
value: "@imnyang",
href: "https://vrchat.com/home/user/usr_d53ac697-182d-40d0-9fd8-1a7442b0ebfd",
icon: "vrchat",
tone: "blue",
},
],
},
];
// biome-ignore lint/correctness/noUnusedVariables: Used in the Astro template below.
const imageIcons: Record<string, { src: string; monochrome: boolean }> = {
github: { src: "/icon/github.svg", monochrome: true },
discord: { src: "/icon/discord.svg", monochrome: true },
mizuki: { src: "/icon/mizuki.svg", monochrome: false },
// Source: https://github.com/simple-icons/simple-icons/blob/develop/icons/steam.svg
steam: { src: "/icon/steam.svg", monochrome: true },
maimai: { src: "/icon/maimai.webp", monochrome: false },
vrchat: { src: "/icon/vrchat.svg", monochrome: true },
// Source: https://pjsekai.sega.jp/assets/data/webp/icon/icon_app.png.webp
"project-sekai": { src: "/icon/project-sekai.webp", monochrome: false },
};
// biome-ignore lint/correctness/noUnusedVariables: Used in the Astro template below.
const paths: Record<string, string> = {
mail: "M22 12a10 10 0 1 0-4 8 M16 8v5a3 3 0 0 0 6 0v-1 M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0",
x: "M4 3h4l12 18h-4L4 3Z M20 3l-7 8 M4 21l7-8",
instagram:
"M7 3h10a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4Z M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0 M17.5 6.5h.01",
youtube:
"M7 5h10a5 5 0 0 1 5 5v4a5 5 0 0 1-5 5H7a5 5 0 0 1-5-5v-4a5 5 0 0 1 5-5Z M10 9l5 3-5 3V9Z",
blog: "M5 3h11l3 3v15H5V3Z M15 3v4h4 M9 11h6 M9 15h6",
};
---
<section id="contact" class="contact-section" aria-labelledby="contact-title">
<div class="contact-content">
<header class="contact-heading">
<h2 id="contact-title">Contact<span aria-hidden="true">.</span></h2>
<p class="contact-description">
새로운 이야기와 재미있는 만남은 언제나 환영해요.
</p>
</header>
<div class="contact-groups">
{
groups.map((group) => (
<section aria-labelledby={`contact-${group.id}`}>
<h3
id={`contact-${group.id}`}
class="contact-group-title"
>
{group.title}
</h3>
<ul class="contact-list">
{group.items.map((item) => (
<li class="contact-row">
<a
class="contact-link"
href={item.href}
target={
"email" in item
? undefined
: "_blank"
}
rel={
"email" in item
? undefined
: "noopener noreferrer"
}
>
<span
class={`contact-icon tone-${item.tone}`}
aria-hidden="true"
>
{imageIcons[item.icon] ? (
<img
src={
imageIcons[item.icon]
.src
}
width="22"
height="22"
alt=""
class:list={{
"mono-icon":
imageIcons[
item.icon
].monochrome,
}}
/>
) : (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d={paths[item.icon]}
/>
</svg>
)}
</span>
<span class="contact-name">
{item.name}
</span>
<span class="contact-value">
{item.value}
</span>
{!("email" in item) && (
<svg
class="contact-arrow"
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 17 17 7M7 7h10v10" />
</svg>
)}
</a>
{"email" in item && (
<button
class="contact-copy"
type="button"
data-contact-email={item.value}
aria-label={`${item.value} 복사`}
title="이메일 주소 복사"
>
<svg
class="copy-symbol"
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
x="8"
y="8"
width="12"
height="12"
rx="2"
/>
<path d="M16 8V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4" />
</svg>
<svg
class="copied-symbol"
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m5 12 4 4L19 6" />
</svg>
</button>
)}
</li>
))}
</ul>
</section>
))
}
</div>
<div class="contact-footer">
<p>여기까지 와주셔서 고마워요.</p>
<a href="#home">맨 위로 <span aria-hidden="true">↑</span></a>
</div>
<p
class="contact-status"
role="status"
aria-live="polite"
aria-atomic="true"
>
</p>
</div>
</section>
<style>
.contact-section {
--contact-top-spacing: clamp(48px, 7vw, 88px);
min-height: 100%;
/* A viewport-spanning snap area permits free scrolling through the links. */
scroll-snap-align: start;
padding: var(--contact-top-spacing) clamp(24px, 5.208vw, 100px) 32px;
}
.contact-content {
max-width: 1080px;
margin-inline: auto;
}
.contact-heading {
display: flow-root;
/* Keep the snap target smaller than the viewport so entry stops at the title. */
scroll-snap-align: start;
scroll-snap-stop: always;
scroll-margin-top: var(--contact-top-spacing);
padding-bottom: 32px;
border-bottom: 1px solid var(--border);
}
.contact-eyebrow {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.16em;
color: var(--muted-foreground);
}
h2 {
margin-top: 8px;
font-size: clamp(36px, 5vw, 52px);
font-weight: 750;
letter-spacing: -0.045em;
line-height: 1.15;
}
h2 span {
color: #dc6291;
}
.contact-description {
margin-top: 14px;
font-size: 14px;
color: color-mix(in srgb, var(--foreground) 65%, var(--background));
word-break: keep-all;
}
.contact-groups {
display: grid;
gap: 28px;
margin-top: 28px;
}
.contact-group-title {
margin-bottom: 12px;
font-size: 15px;
font-weight: 700;
}
.contact-list {
display: grid;
gap: 8px;
}
.contact-row {
display: flex;
min-width: 0;
border: 1px solid var(--border);
border-radius: 9px;
transition:
background-color 160ms,
border-color 160ms;
}
.contact-row:hover,
.contact-row:focus-within {
background: var(--muted);
border-color: color-mix(
in srgb,
var(--foreground) 22%,
var(--background)
);
}
.contact-link {
display: grid;
grid-template-columns: 22px auto minmax(0, 1fr) 18px;
gap: 12px;
align-items: center;
flex: 1;
min-width: 0;
min-height: 58px;
padding: 12px 18px;
border-radius: 8px;
text-decoration: none;
}
.contact-row:has(.contact-copy) .contact-link {
grid-template-columns: 22px auto minmax(0, 1fr);
padding-right: 0;
}
.contact-icon {
display: flex;
align-items: center;
justify-content: center;
}
.contact-icon svg,
.contact-icon img {
width: 21px;
height: 21px;
object-fit: contain;
}
.mono-icon {
filter: var(--mono-filter);
}
.tone-pink {
color: #ce5484;
}
.tone-green {
color: #46916c;
}
.tone-purple {
color: #8279e2;
}
.tone-blue {
color: #569fc9;
}
.tone-red {
color: #d96570;
}
.tone-orange {
color: #c58055;
}
.contact-name {
font-size: 14px;
font-weight: 650;
}
.contact-value {
text-align: right;
font-size: 14px;
color: color-mix(in srgb, var(--foreground) 65%, var(--background));
overflow-wrap: anywhere;
}
.contact-arrow {
width: 18px;
height: 18px;
color: var(--muted-foreground);
transition:
transform 160ms,
color 160ms;
}
.contact-link:hover .contact-arrow {
transform: translate(2px, -2px);
color: var(--foreground);
}
.contact-copy {
display: grid;
place-items: center;
align-self: center;
width: 44px;
height: 44px;
margin: 0 5px;
flex-shrink: 0;
border-radius: 6px;
color: var(--muted-foreground);
cursor: pointer;
}
.contact-copy:hover {
color: var(--foreground);
background: color-mix(in srgb, var(--foreground) 7%, transparent);
}
.contact-copy svg {
width: 17px;
height: 17px;
}
.copied-symbol,
.contact-copy[data-copied] .copy-symbol {
display: none;
}
.contact-copy[data-copied] .copied-symbol {
display: block;
color: #46916c;
}
.contact-link:focus-visible,
.contact-copy:focus-visible,
.contact-footer a:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 3px;
}
.contact-footer {
display: flex;
justify-content: space-between;
gap: 16px;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--muted-foreground);
}
.contact-footer a {
flex-shrink: 0;
border-radius: 2px;
}
.contact-footer a:hover {
color: var(--foreground);
}
.contact-footer span {
margin-left: 8px;
}
.contact-status {
min-height: 20px;
margin-top: 12px;
font-size: 12px;
color: var(--foreground);
text-align: right;
}
@media (max-width: 540px) {
.contact-link,
.contact-row:has(.contact-copy) .contact-link {
grid-template-columns: 22px minmax(0, 1fr) 18px;
gap: 4px 12px;
padding: 14px;
}
.contact-row:has(.contact-copy) .contact-link {
grid-template-columns: 22px minmax(0, 1fr);
padding-right: 0;
}
.contact-icon {
grid-row: 1 / 3;
}
.contact-name {
grid-column: 2;
font-size: 13px;
}
.contact-value {
grid-column: 2;
grid-row: 2;
text-align: left;
font-size: 12px;
}
.contact-arrow {
grid-column: 3;
grid-row: 1 / 3;
}
}
@media (prefers-reduced-motion: reduce) {
.contact-row,
.contact-arrow {
transition: none;
}
.contact-link:hover .contact-arrow {
transform: none;
}
}
</style>
<script>
const contactStatus =
document.querySelector<HTMLElement>(".contact-status");
document
.querySelectorAll<HTMLButtonElement>("[data-contact-email]")
.forEach((button) => {
let resetTimer: ReturnType<typeof setTimeout>;
button.addEventListener("click", async () => {
const email = button.dataset.contactEmail;
if (!email) return;
try {
await navigator.clipboard.writeText(email);
clearTimeout(resetTimer);
button.dataset.copied = "true";
button.setAttribute("aria-label", `${email} 복사 완료`);
if (contactStatus)
contactStatus.textContent = `${email} 주소를 복사했어요.`;
resetTimer = setTimeout(() => {
delete button.dataset.copied;
button.setAttribute("aria-label", `${email} 복사`);
}, 2000);
} catch {
if (contactStatus)
contactStatus.textContent =
"복사하지 못했어요. 이메일 주소를 직접 선택해 복사해주세요.";
}
});
});
</script>

View file

@ -1,27 +1,31 @@
---
export interface Props {
targetDate: string;
label: string;
targetDate: string;
label?: string;
}
const { targetDate, label } = Astro.props;
---
<div class="flex flex-col items-end text-right text-sm">
<span id="dday-label">D-Day</span>
<span class="text-muted-foreground">{label} | {new Date(targetDate).toDateString()}</span>
<div class="flex flex-row items-start text-left text-base gap-2">
<span class="text-foreground"
>{label && `${label} | `}{
new Date(targetDate).toLocaleDateString()
}</span
>
<span id="dday-label">D-Day</span>
</div>
<script define:vars={{ targetDate }}>
const today = new Date();
const target = new Date(targetDate);
const diffTime = target.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
let label = "D-Day";
if (diffDays > 0) label = `D-${diffDays}`;
else if (diffDays < 0) label = `D+${Math.abs(diffDays)}`;
const el = document.getElementById('dday-label');
if (el) el.textContent = label;
const today = new Date();
const target = new Date(targetDate);
const diffTime = target.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
let label = "D-Day";
if (diffDays > 0) label = `D-${diffDays}`;
else if (diffDays < 0) label = `D+${Math.abs(diffDays)}`;
const el = document.getElementById("dday-label");
if (el) el.textContent = label;
</script>

View file

@ -125,12 +125,12 @@ const roles = ["Broadcast Manager", "ARG Creator", "Developer"];
<button
class="group relative block h-full w-full cursor-pointer border-0 bg-transparent p-0 text-white focus-visible:outline-2 focus-visible:outline-offset-[-5px] focus-visible:outline-[#ff92ad]"
type="button"
aria-label="EPC Finale 결승 방송을 2시간 19분 26초부터 재생"
aria-label="EPC Finale 후기 영상 재생"
>
<img
class="block h-full w-full object-cover"
src="/projects/epc-finale-broadcast.avif"
alt="EPC Finale의 두 선수 경기 화면과 실시간 판정을 보여주는 방송 화면"
src="/projects/epc-finale.avif"
alt="썸네일"
width="655"
height="387"
loading="lazy"
@ -142,11 +142,8 @@ const roles = ["Broadcast Manager", "ARG Creator", "Developer"];
>
<span
class="absolute inset-x-0 bottom-0 flex justify-between gap-2 bg-gradient-to-b from-transparent to-[#000c] px-4 pt-[30px] pb-3 text-left text-[clamp(10px,1vw,14px)] font-semibold"
>Finale 결승 방송 <span
class="font-normal opacity-80"
>02:19:26부터 재생</span
></span
>
>후기 영상
</span>
</button>
</div>
<img
@ -206,9 +203,8 @@ const roles = ["Broadcast Manager", "ARG Creator", "Developer"];
if (!videoFrame) return;
const player = document.createElement("iframe");
player.src =
"https://www.youtube-nocookie.com/embed/5HDW7u3xt6Y?start=8366&autoplay=1&playsinline=1";
player.title =
"EPC Finale Playing Section Tournament Finals — 2:19:26부터 재생";
"https://www.youtube-nocookie.com/embed/3NY8vgxvKaU?autoplay=1&playsinline=1";
player.title = "EPC : Finale 방송화면에 이런 비밀이???";
player.allow =
"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share";
player.allowFullscreen = true;

View file

@ -24,20 +24,19 @@ const daysUntilYearEnd = Math.ceil(
---
<section
id="about"
id="experience"
data-node-id="4:104"
class="min-h-full snap-start snap-always bg-background text-foreground"
aria-labelledby="about-title"
aria-labelledby="experience-title"
>
<div class="grid min-h-full grid-cols-1 lg:grid-cols-2">
<div
class="relative z-10 px-6 py-12 sm:px-10 lg:px-[clamp(52px,5.208vw,100px)] lg:py-[clamp(58px,9.26vh,100px)]"
>
<div
id="experience"
class="max-w-[654px] break-keep text-[24px] leading-[1.2] tracking-[-0.02em] lg:leading-none"
>
<p id="about-title">
<p id="experience-title">
저는 <b>{years.length}년간 {events.length}개</b>의 개성있는
조각들을 모아왔어요.
</p>

View file

@ -1,6 +1,8 @@
---
import About from "@/components/About.astro";
import ContactSection from "@/components/ContactSection.astro";
import EPCProjects from "@/components/EPCProjects.astro";
import Experience from "@/components/Experience.astro";
import Header from "@/components/Header.astro";
import Hero from "@/components/Hero.astro";
import "@/globals.css";
@ -39,6 +41,8 @@ import "@/globals.css";
<Hero />
<About />
<EPCProjects />
<Experience />
<ContactSection />
</main>
</body>
</html>