PageSourceSearch

https://saii.kr/assets/controllers/birthdate_picker_controller-be8baf30.js

js saii.kr collected 2026-10-06 03:44:20 UTC 2,751 bytes, 89 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// Year / month / day birthdate picker.
4//
5// Year-first ordering matches how Koreans anchor age ("몇 년생?") and stays
6// unambiguous for international buddies, unlike a native date input that
7// renders in the browser's locale format. Three <select>s drive a single
8// hidden user[birthdate] field (YYYY-MM-DD) so the backend contract is
9// unchanged, and a live 만 나이 readout keeps the 18+ gate transparent.
10export default class extends Controller {
11  static targets = ["year", "month", "day", "hidden", "readout"]
12  static values = {
13    minAge: { type: Number, default: 18 },
14    today: String, // YYYY-MM-DD, injected so age math matches the server clock
15    prompt: String,
16    validTemplate: String, // uses %{age}
17    underage: String,
18  }
19
20  connect() {
21    this.syncDays()
22    this.sync()
23  }
24
25  change() {
26    this.syncDays()
27    this.sync()
28  }
29
30  // Trim day options that don't exist in the chosen month/year (e.g. Feb 30),
31  // clamping a now-invalid selection down to the last real day.
32  syncDays() {
33    const year = Number(this.yearTarget.value) || this.todayParts().year
34    const month = Number(this.monthTarget.value)
35    const max = month ? new Date(year, month, 0).getDate() : 31
36
37    this.dayTarget.querySelectorAll("option[value]").forEach((option) => {
38      if (!option.value) return
39      option.hidden = Number(option.value) > max
40    })
41
42    if (Number(this.dayTarget.value) > max) this.dayTarget.value = String(max)
43  }
44
45  sync() {
46    const year = this.yearTarget.value
47    const month = this.monthTarget.value
48    const day = this.dayTarget.value
49
50    if (!year || !month || !day) {
51      this.hiddenTarget.value = ""
52      this.paint(this.promptValue, "neutral")
53      return
54    }
55
56    const iso = `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`
57    this.hiddenTarget.value = iso
58
59    const age = this.ageFrom(Number(year), Number(month), Number(day))
60    if (age < this.minAgeValue) {
61      this.paint(this.underageValue, "safety")
62    } else {
63      this.paint(this.validTemplateValue.replace("%{age}", age), "confirm")
64    }
65  }
66
67  ageFrom(year, month, day) {
68    const { year: ty, month: tm, day: td } = this.todayParts()
69    let age = ty - year
70    if (tm < month || (tm === month && td < day)) age -= 1
71    return age
72  }
73
74  todayParts() {
75    const [year, month, day] = this.todayValue.split("-").map(Number)
76    return { year, month, day }
77  }
78
79  paint(text, tone) {
80    if (!this.hasReadoutTarget) return
81    this.readoutTarget.textContent = text
82    const colors = {
83      neutral: "var(--kb-caption)",
84      confirm: "var(--kb-ink)",
85      safety: "var(--kb-safety-deep)",
86    }
87    this.readoutTarget.style.color = colors[tone]
88  }
89}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.