LogoSkills

feature:presentation

Clean Architecture Presentation Layer generation (BLoC, Page, Widget, Tests, Widgetbook)

ํ•ญ๋ชฉ๋‚ด์šฉ
Invoke/feature:presentation
Aliases/presentation, /presentation:create
Categorykobic-workflow
Complexitymoderate
MCP Serversserena, context7, magic

/feature:presentation โ€” ํ™”๋ฉด(UI) ํ•œ ์„ธํŠธ ์ž๋™ ์ƒ์„ฑ#

ํ•œ๋งˆ๋””๋กœ#

ํ™”๋ฉด์„ ๋งŒ๋“ค ๋•Œ ํ•„์š”ํ•œ ๋ถ€ํ’ˆ๋“ค(ํ™”๋ฉด ํ‹€, ๋ฒ„ํŠผยท์นด๋“œ ๊ฐ™์€ ์œ„์ ฏ, ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ์—ฐ๊ฒฐํ•˜๋Š” ๋กœ์ง, ํ…Œ์ŠคํŠธ, ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์นดํƒˆ๋กœ๊ทธ)์„ ํ•œ ๋ฒˆ์— ์ž๋™์œผ๋กœ ์ฐ์–ด๋‚ด๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ๊ฐ€๊ตฌ ์กฐ๋ฆฝ์ฒ˜๋Ÿผ, ํ™”๋ฉด์„ ๋งŒ๋“ค ๋•Œ๋งˆ๋‹ค ๋งค๋ฒˆ ์†์œผ๋กœ ๊นŽ๋˜ ๋ถ€ํ’ˆ์„ ์ •ํ•ด์ง„ ๊ทœ๊ฒฉ๋Œ€๋กœ ํ•œ ์„ธํŠธ๋กœ ๋ฝ‘์•„์ค€๋‹ค๊ณ  ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋ˆ„๊ฐ€ยท์–ธ์ œ ์“ฐ๋‚˜์š”#

  • ์ƒˆ ๊ธฐ๋Šฅ์˜ ํ™”๋ฉด(UI) ๋ถ€๋ถ„์„ ์ƒˆ๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•  ๋•Œ
  • ๋ชฉ๋กยท์ƒ์„ธยท์ž‘์„ฑ ๊ฐ™์€ ํŽ˜์ด์ง€์™€, ํ™”๋ฉด ๋™์ž‘ ๋กœ์ง(BLoC), ํ™”๋ฉด ์ด๋™ ๊ฒฝ๋กœ๋ฅผ ํ•œ๊บผ๋ฒˆ์— ๋งŒ๋“ค๊ณ  ์‹ถ์„ ๋•Œ
  • ์ „์ฒด ๊ธฐ๋Šฅ์„ ์ž๋™์œผ๋กœ ๋งŒ๋“œ๋Š” /feature:create ์ž‘์—…์˜ 5๋ฒˆ์งธ ๋‹จ๊ณ„๋กœ ์ž๋™ ํ˜ธ์ถœ๋  ๋•Œ

๋ฌด์—‡์„ ํ•ด์ฃผ๋‚˜์š”#

๋ช…๋ น ํ•œ ๋ฒˆ์œผ๋กœ ํ™”๋ฉด ๊ด€๋ จ ํŒŒ์ผ๋“ค์ด ์ •ํ•ด์ง„ ํด๋”์— ์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๋งŒ๋“ค์–ด์ง€๋Š” ๊ฒƒ๋“ค:

  • ํ™”๋ฉด ๋™์ž‘ ๋กœ์ง โ€” {feature}_list_bloc.dart, {feature}_list_event.dart, {feature}_list_state.dart (๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ํ™”๋ฉด ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌ)
  • ํŽ˜์ด์ง€ โ€” {feature}_page.dart (์‹ค์ œ ๋ณด์ด๋Š” ํ™”๋ฉด ํ‹€)
  • ์œ„์ ฏ โ€” {entity}_card.dart (๋ชฉ๋ก์˜ ์นด๋“œ์ฒ˜๋Ÿผ ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•˜๋Š” ์ž‘์€ ๋ถ€ํ’ˆ)
  • ํ™”๋ฉด ์ด๋™ ๊ฒฝ๋กœ โ€” {feature}_route.dart
  • ํ…Œ์ŠคํŠธ โ€” ๋™์ž‘ ๋กœ์งยทํ™”๋ฉด์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ์ฝ”๋“œ
  • ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์นดํƒˆ๋กœ๊ทธ โ€” {entity}_card_use_case.dart (Widgetbook์—์„œ ๋ถ€ํ’ˆ์„ ๋”ฐ๋กœ ๋ฏธ๋ฆฌ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ)
  • (์„ ํƒ) BDD ์‹œ๋‚˜๋ฆฌ์˜ค โ€” ์‚ฌ์šฉ์ž ๊ด€์ ์˜ ๋™์ž‘ ๋ช…์„ธ ํŒŒ์ผ

์–ด๋–ป๊ฒŒ ์“ฐ๋‚˜์š”#

# ๊ธฐ๋ณธ: ๊ธฐ๋Šฅ ์ด๋ฆ„๊ณผ ๋ฐ์ดํ„ฐ ์ด๋ฆ„์„ ๋„ฃ์–ด ํ˜ธ์ถœ
/feature:presentation community Post

# ๋งŒ๋“ค ์œ„์น˜๋ฅผ ์ง€์ • (application / common / console ์ค‘ ์„ ํƒ, ๊ธฐ๋ณธ๊ฐ’ application)
/feature:presentation chat Message --location console

# ๋งŒ๋“ค ํŽ˜์ด์ง€ ๋ชฉ๋ก์„ ์ง์ ‘ ์ง€์ •
/feature:presentation community Post --pages  " list, detail, create "
  • community, chat ์ฒ˜๋Ÿผ ๊ธฐ๋Šฅ ์ด๋ฆ„์„ ๋จผ์ € ๋„ฃ์Šต๋‹ˆ๋‹ค (์†Œ๋ฌธ์ž+๋ฐ‘์ค„ ํ˜•์‹).
  • Post, Message ์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ(์—”ํ‹ฐํ‹ฐ) ์ด๋ฆ„์„ ๋„ฃ์Šต๋‹ˆ๋‹ค (์ฒซ ๊ธ€์ž ๋Œ€๋ฌธ์ž ํ˜•์‹).
  • --location ์œผ๋กœ ์–ด๋””์— ๋งŒ๋“ค์ง€, --pages ๋กœ ์–ด๋–ค ํŽ˜์ด์ง€๋“ค์„ ๋งŒ๋“ค์ง€ ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•ˆ์—์„œ ๋ฌด์Šจ ์ผ์ด ๋ฒŒ์–ด์ง€๋‚˜์š”#

ํฌ๊ฒŒ ๋‹ค์Œ ์ˆœ์„œ๋กœ ํ™”๋ฉด ๋ถ€ํ’ˆ๋“ค์„ ์ฐจ๋ก€์ฐจ๋ก€ ๋งŒ๋“ค์–ด๋ƒ…๋‹ˆ๋‹ค.

  1. ๊ธฐ์กด ๋ฐฉ์‹ ์‚ดํŽด๋ณด๊ธฐ โ€” ์ด๋ฏธ ์žˆ๋Š” ๋น„์Šทํ•œ ํ™”๋ฉด ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•ด ๊ฐ™์€ ์Šคํƒ€์ผ๋กœ ๋งž์ถฅ๋‹ˆ๋‹ค.
  2. ํ™”๋ฉด ๋™์ž‘ ๋กœ์ง ๋งŒ๋“ค๊ธฐ โ€” ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ (๋กœ๋”ฉยท์„ฑ๊ณตยท์‹คํŒจ) ํ™”๋ฉด ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” BLoC ๋ถ€๋ถ„์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  3. ๋กœ์ง ํ…Œ์ŠคํŠธ ๋งŒ๋“ค๊ธฐ โ€” ๊ทธ ๋™์ž‘ ๋กœ์ง์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ํ•จ๊ป˜ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  4. ํŽ˜์ด์ง€์™€ ์œ„์ ฏ ๋งŒ๋“ค๊ธฐ โ€” ์‹ค์ œ ๋ณด์ด๋Š” ํ™”๋ฉด ํ‹€๊ณผ, ์นด๋“œ ๊ฐ™์€ ์ž‘์€ ํ™”๋ฉด ๋ถ€ํ’ˆ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  5. ํ™”๋ฉด ํ…Œ์ŠคํŠธ ๋งŒ๋“ค๊ธฐ โ€” ํ™”๋ฉด์ด ์˜๋„๋Œ€๋กœ ๊ทธ๋ ค์ง€๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  6. ํ™”๋ฉด ์ด๋™ ๊ฒฝ๋กœ ๋งŒ๋“ค๊ธฐ โ€” ์ด ํ™”๋ฉด์œผ๋กœ ์–ด๋–ป๊ฒŒ ์ด๋™ํ•˜๋Š”์ง€ ๊ฒฝ๋กœ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  7. ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์นดํƒˆ๋กœ๊ทธ ๋“ฑ๋ก โ€” ๋งŒ๋“  ๋ถ€ํ’ˆ์„ ๋”ฐ๋กœ ๋ฏธ๋ฆฌ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ Widgetbook์— ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
  8. (์„ ํƒ) BDD ์‹œ๋‚˜๋ฆฌ์˜ค ์—ฐ๋™ โ€” ์‚ฌ์šฉ์ž ๊ด€์ ์˜ ๋™์ž‘ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋ณ„๋„๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

โš™๏ธ ์ƒ์„ธ ์˜ต์…˜ยท์‹คํ–‰ ๋ช…์„ธ (๊ฐœ๋ฐœ์ž / AI ์—์ด์ „ํŠธ์šฉ)

Triggers#

  • When a new Feature Presentation Layer is needed
  • When BLoC, Page, Widget, and Route generation are needed
  • /feature:create orchestration Step 5

Context Trigger Pattern#

/feature:presentation {feature_name} {entity_name} [--options]

Parameters#

ParameterRequiredDescriptionExample
feature_nameโœ…Feature module name (snake_case)community, chat
entity_nameโœ…Entity name (PascalCase)Post, Message
--locationโŒLocationapplication, common, console (default: application)
--pagesโŒPage list to generate"list, detail, create"

Behavioral Flow#

1. Existing Pattern Analysis#

Analyze existing Presentation Layer patterns using Serena MCP:
- feature/application/community/lib/src/presentation/bloc/post_list/post_list_bloc.dart
- feature/application/community/lib/src/presentation/bloc/post_list/post_list_event.dart
- feature/application/community/lib/src/presentation/bloc/post_list/post_list_state.dart

2. BLoC Event Generation (sealed class + private implementation)#

part of '{feature}_list_bloc.dart';

/// {Feature} ๋ชฉ๋ก ์ด๋ฒคํŠธ
@immutable
sealed class {Feature}ListEvent {
  const {Feature}ListEvent();

  // Factory constructors (Public API)
  const factory {Feature}ListEvent.loadRequested() = _LoadRequested;
  const factory {Feature}ListEvent.refreshRequested() = _RefreshRequested;
  const factory {Feature}ListEvent.categoryChanged({
    {Entity}Category? category,
  }) = _CategoryChanged;
}

// Private implementation classes
@immutable
final class _LoadRequested extends {Feature}ListEvent {
  const _LoadRequested();
}

@immutable
final class _RefreshRequested extends {Feature}ListEvent {
  const _RefreshRequested();
}

@immutable
final class _CategoryChanged extends {Feature}ListEvent {
  const _CategoryChanged({this.category});
  final {Entity}Category? category;
}

3. BLoC State Generation#

part of '{feature}_list_bloc.dart';

/// {Feature} ๋ชฉ๋ก ์ƒํƒœ
@immutable
sealed class {Feature}ListState {
  const {Feature}ListState({
    required this.{entity}s,
    required this.currentSort,
    this.currentCategory,
  });

  final List<{Entity}> {entity}s;
  final {Entity}Category? currentCategory;
  final {Entity}SortType currentSort;
}

@immutable
final class {Feature}ListInitial extends {Feature}ListState {
  const {Feature}ListInitial()
      : super({entity}s: const [], currentSort: {Entity}SortType.latest);
}

@immutable
final class {Feature}ListLoading extends {Feature}ListState { ... }

@immutable
final class {Feature}ListLoaded extends {Feature}ListState {
  // ... hasMore, total ์ถ”๊ฐ€
  // copyWith ๋ฉ”์„œ๋“œ ํฌํ•จ
}

@immutable
final class {Feature}ListError extends {Feature}ListState {
  // ... message ์ถ”๊ฐ€
}

4. BLoC Class Generation (Optional Constructor Injection)#

import 'package:dependencies/dependencies.dart';

part '{feature}_list_event.dart';
part '{feature}_list_state.dart';

/// {Feature} ๋ชฉ๋ก BLoC
class {Feature}ListBloc extends Bloc<{Feature}ListEvent, {Feature}ListState> {
  {Feature}ListBloc({
    Get{Entity}sUsecase? get{Entity}sUsecase,
  }) : _get{Entity}sUsecase = get{Entity}sUsecase ?? const Get{Entity}sUsecase(),
       super(const {Feature}ListInitial()) {
    on<_LoadRequested>(_onLoadRequested);
    on<_RefreshRequested>(_onRefreshRequested);
  }

  final Get{Entity}sUsecase _get{Entity}sUsecase;

  Future<void> _onLoadRequested(
    _LoadRequested event,
    Emitter<{Feature}ListState> emit,
  ) async {
    emit({Feature}ListLoading(...));

    // โœ… UseCase call
    final result = await _get{Entity}sUsecase(
      Get{Entity}sParams(
        limit: _pageSize,
        offset: _currentOffset,
        category: state.currentCategory,
      ),
    );

    result.fold(
      (failure) {
        if (!isClosed) {  // โœ… BLoC closed check
          emit({Feature}ListError(message: failure.message ?? '์˜ค๋ฅ˜'));
        }
      },
      ({entity}ListResult) {
        if (!isClosed) {
          emit({Feature}ListLoaded(...));
        }
      },
    );
  }
}

5. BLoC Test Generation#

void main() {
  late MockGet{Entity}sUsecase mockUsecase;

  setUpAll(registerFallbackValues);

  setUp(() {
    mockUsecase = MockGet{Entity}sUsecase();
  });

  blocTest<{Feature}ListBloc, {Feature}ListState>(
    '๋กœ๋“œ ์„ฑ๊ณต ์‹œ {Feature}ListLoaded ์ƒํƒœ',
    build: () => {Feature}ListBloc(
      get{Entity}sUsecase: mockUsecase,  // โœ… Direct mock injection
    ),
    setUp: () {
      when(() => mockUsecase(any()))
        .thenAnswer((_) async => Right(testResult));
    },
    act: (bloc) => bloc.add(const {Feature}ListEvent.loadRequested()),
    expect: () => [
      isA<{Feature}ListLoading>(),
      isA<{Feature}ListLoaded>(),
    ],
  );
}

6. Page Generation (BlocProvider wrapping)#

class {Feature}Page extends StatelessWidget {
  const {Feature}Page({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) =>
          {Feature}ListBloc()..add(const {Feature}ListEvent.loadRequested()),
      child: const {Feature}View(),
    );
  }
}

class {Feature}View extends StatelessWidget {
  const {Feature}View({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('{Feature}')),
      body: BlocBuilder<{Feature}ListBloc, {Feature}ListState>(
        builder: (context, state) {
          return switch (state) {
            {Feature}ListInitial() => const SizedBox.shrink(),
            {Feature}ListLoading() => const Center(child: CircularProgressIndicator()),
            {Feature}ListLoaded(:final {entity}s) => ListView.builder(...),
            {Feature}ListError(:final message) => Center(child: Text('์˜ค๋ฅ˜: $message')),
          };
        },
      ),
    );
  }
}

7. Widget Generation (super.key last)#

class {Entity}Card extends StatelessWidget {
  const {Entity}Card({
    required this.{entity},
    this.onTap,
    super.key,  // โœ… Always last
  });

  final {Entity} {entity};
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) { ... }
}

8. Widget Test Generation#

testWidgets('{์—”ํ‹ฐํ‹ฐ} ๋ชฉ๋ก ํ‘œ์‹œ', (tester) async {
  when(() => mockRepository.get{Entity}s(...))
    .thenAnswer((_) async => Right(testResult));

  await tester.pumpWidget(const MaterialApp(home: {Feature}Page()));
  await tester.pumpAndSettle();

  expect(find.byType({Entity}Card), findsNWidgets(2));
});

9. Route Generation#

@TypedGoRoute<{Feature}Route>(path: '/{feature}')
class {Feature}Route extends GoRouteData with ${Feature}Route {
  const {Feature}Route();

  static RouteBase get base => ${feature}Route;

  @override
  MaterialPage<void> buildPage(BuildContext context, GoRouterState state) {
    return const MaterialPage<void>(child: {Feature}Page());
  }
}

abstract class {Feature}RouteName {
  static const String path = '/{feature}';
}

10. Generate Widgetbook UseCase#

@widgetbook.UseCase(name: 'Default', type: {Entity}Card)
Widget build{Entity}CardUseCase(BuildContext context) {
  return {Entity}Card(
    {entity}: const {Entity}(id: 1, title: 'ํ…Œ์ŠคํŠธ', ...),
  );
}

11. BDD Test Integration (Optional)#

/bdd:generate generated separately via command:

# Generate BDD tests
/bdd:generate {feature_name} --location {location}

Generation๋˜๋Š” BDD File:

feature/{location}/{feature_name}/test/src/bdd/
โ”œโ”€โ”€ {feature}_list.feature
โ”œโ”€โ”€ {feature}_detail.feature
โ”œโ”€โ”€ {feature}_form.feature
โ”œโ”€โ”€ step/
โ”‚   โ””โ”€โ”€ {feature}_steps.dart
โ””โ”€โ”€ hooks/
    โ””โ”€โ”€ hooks.dart

BDD Scenario Examples:

Feature: {feature} List # {feature} ๋ชฉ๋ก
  As a user, I want to view the {feature} list. # ์‚ฌ์šฉ์ž๋กœ์„œ {feature} ๋ชฉ๋ก์„ ๋ณด๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค

  @smoke
  Scenario: List loads successfully # ๋ชฉ๋ก ๋กœ๋”ฉ ์„ฑ๊ณต
    Given the app is running # ์•ฑ์ด ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค
    When I navigate to the {feature} page # {feature} ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค
    Then the {feature} list is displayed # {feature} ๋ชฉ๋ก์ด ๋ณด์ž…๋‹ˆ๋‹ค

Output Files#

feature/{location}/{feature_name}/lib/src/presentation/
โ”œโ”€โ”€ bloc/{feature}_list/
โ”‚   โ”œโ”€โ”€ {feature}_list_bloc.dart
โ”‚   โ”œโ”€โ”€ {feature}_list_event.dart
โ”‚   โ””โ”€โ”€ {feature}_list_state.dart
โ”œโ”€โ”€ page/
โ”‚   โ””โ”€โ”€ {feature}_page.dart
โ”œโ”€โ”€ widget/
โ”‚   โ””โ”€โ”€ {entity}_card.dart
โ””โ”€โ”€ route/
    โ””โ”€โ”€ {feature}_route.dart

feature/{location}/{feature_name}/test/
โ”œโ”€โ”€ presentation/
โ”‚   โ”œโ”€โ”€ bloc/{feature}_list_bloc_test.dart
โ”‚   โ””โ”€โ”€ page/{feature}_page_test.dart
โ””โ”€โ”€ src/bdd/                               # BDD ํ…Œ์ŠคํŠธ (์„ ํƒ)
    โ”œโ”€โ”€ {feature}_list.feature
    โ”œโ”€โ”€ {feature}_detail.feature
    โ”œโ”€โ”€ {feature}_form.feature
    โ”œโ”€โ”€ step/{feature}_steps.dart
    โ””โ”€โ”€ hooks/hooks.dart

app/kobic_widgetbook/lib/src/{feature_name}/
โ””โ”€โ”€ {entity}_card_use_case.dart

MCP Integration#

  • Serena: Existing Presentation Layer Pattern Analysis
  • Context7: BLoC, GoRouter Document reference
  • Magic (21st.dev): UI ์ปดํฌ๋„ŒํŠธ Generation, ์ ‘๊ทผ์„ฑ ๊ฒ€์‚ฌ

Reference Agents#

Detailed implementation rules in ${CLAUDE_PLUGIN_ROOT}/commands/agents/app/presentation-layer-agent.md reference

Core Rules Summary#

โœ… BLoC Pattern#

// Event: sealed class + factory + private implementation
sealed class {Feature}ListEvent {
  const factory {Feature}ListEvent.loadRequested() = _LoadRequested;
}

final class _LoadRequested extends {Feature}ListEvent {
  const _LoadRequested();
}

// BLoC: Optional Constructor Injection
class {Feature}ListBloc extends Bloc<...> {
  {Feature}ListBloc({
    Get{Entity}sUsecase? get{Entity}sUsecase,
  }) : _get{Entity}sUsecase = get{Entity}sUsecase ?? const Get{Entity}sUsecase(),
       super(...);

  final Get{Entity}sUsecase _get{Entity}sUsecase;

  Future<void> _onLoadRequested(...) async {
    final result = await _get{Entity}sUsecase(params);
    if (!isClosed) { emit(...); }  // โœ… Closed check
  }
}

โŒ Prohibited Patterns#

// โŒ getIt usage prohibited
// create: (_) => getIt<{Feature}Bloc>(),

// โŒ Key? key ํŒจํ„ด ๊ธˆ์ง€
// const {Entity}Card({Key? key}) : super(key: key);

Widget super.key Position#

const {Entity}Card({
  required this.{entity},  // required ๋จผ์ €
  this.onTap,              // optional ๋‹ค์Œ
  super.key,               // โœ… super.key ๋งˆ์ง€๋ง‰
});