package app import ( "fmt" "sort" "sync" "time" ) type GuestStatus struct { Name string `json:"name"` Present bool `json:"present"` LastJoin time.Time `json:"last_join"` LastLeave time.Time `json:"last_leave"` AbsentFor string `json:"absent_for"` } type GuestTracker struct { mu sync.RWMutex current string items map[string]*GuestStatus } func NewGuestTracker(names []string) *GuestTracker { items := make(map[string]*GuestStatus, len(names)) for _, name := range names { items[name] = &GuestStatus{Name: name} } return &GuestTracker{items: items} } func (t *GuestTracker) SetCurrentInstance(label string) { t.mu.Lock() defer t.mu.Unlock() t.current = label } func (t *GuestTracker) CurrentInstance() string { t.mu.RLock() defer t.mu.RUnlock() return t.current } func (t *GuestTracker) MarkJoin(name string, at time.Time) { t.mu.Lock() defer t.mu.Unlock() s := t.ensure(name) s.Present = true s.LastJoin = at s.AbsentFor = "" } func (t *GuestTracker) MarkLeave(name string, at time.Time) { t.mu.Lock() defer t.mu.Unlock() s := t.ensure(name) s.Present = false s.LastLeave = at if !s.LastJoin.IsZero() { s.AbsentFor = humanSince(at) } } func (t *GuestTracker) Snapshot(now time.Time) []GuestStatus { t.mu.RLock() defer t.mu.RUnlock() out := make([]GuestStatus, 0, len(t.items)) for _, s := range t.items { cp := *s if !cp.Present && !cp.LastLeave.IsZero() { cp.AbsentFor = humanSince(cp.LastLeave, now) } out = append(out, cp) } sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out } func (t *GuestTracker) ensure(name string) *GuestStatus { if s, ok := t.items[name]; ok { return s } s := &GuestStatus{Name: name} t.items[name] = s return s } func humanSince(at time.Time, now ...time.Time) string { ref := time.Now() if len(now) > 0 { ref = now[0] } if at.IsZero() { return "" } d := ref.Sub(at) if d < time.Minute { return "1分未満" } m := int(d.Minutes()) if m < 60 { return fmt.Sprintf("%d分前", m) } h := m / 60 if h < 24 { return fmt.Sprintf("%d時間前", h) } return fmt.Sprintf("%d日前", h/24) }